diff options
73 files changed, 12766 insertions, 0 deletions
diff --git a/docs/how-to-use/APITests.rst b/docs/how-to-use/APITests.rst new file mode 100644 index 0000000..599325f --- /dev/null +++ b/docs/how-to-use/APITests.rst @@ -0,0 +1,261 @@ +SNAPS OpenStack API Testing +=========================== + +Tests designated as component tests extend the snaps.openstack.tests.OSComponentTestCase class and must be exercised +with OpenStack credentials for all as well as an external network for many. When leveraging the unit\_test\_suite.py +application, the -e argument and -n arguments will suffice. When attempting to execute these tests within your IDE +of choice (tested on IntelliJ), you will need to edit the [repo\_dir]/snaps/openstack/tests/conf/os\_env.yaml file as well +as ensuring that your run configuration's working directory is set to [repo\_dir]/snaps. + +The Test Classes +================ + +glance_utils_tests.py - GlanceSmokeTests +---------------------------------------- + +Ensures that a Glance client can be obtained as well as the proper +exceptions thrown with the wrong credentials. + +keystone_utils_tests.py - KeystoneSmokeTests +-------------------------------------------- + +Ensures that a Keystone client can be obtained as well as the proper +exceptions thrown with the wrong credentials. + +neutron_utils_tests.py - NeutronSmokeTests +------------------------------------------ + +Ensures that a Neutron client can be obtained as well as the proper +exceptions thrown with the wrong credentials. + +nova_utils_tests.py - NovaSmokeTests +------------------------------------ + +Ensures that a Nova client can be obtained as well as the proper +exceptions thrown with the wrong credentials. + +keystone_utils_tests.py - KeystoneUtilsTests +-------------------------------------------- + ++----------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Keystone API | Description | ++==================================+===============+===========================================================+ +| test_create_user_minimal | 2 & 3 | Tests the creation of a user with minimal configuration | +| | | settings via the utility functions | ++----------------------------------+---------------+-----------------------------------------------------------+ +| test_create_project_minimal | 2 & 3 | Tests the creation of a project with minimal configuration| +| | | settings via the utility functions | ++----------------------------------+---------------+-----------------------------------------------------------+ + +create_user_tests.py - CreateUserSuccessTests +--------------------------------------------- ++----------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Keystone API | Description | ++==================================+===============+===========================================================+ +| test_create_user | 2 & 3 | Tests the creation of a user with minimal configuration | +| | | settings via the utility functions | ++----------------------------------+---------------+-----------------------------------------------------------+ + +create_project_tests.py - CreateProjectSuccessTests +--------------------------------------------------- + ++----------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Keystone API | Description | ++==================================+===============+===========================================================+ +| test_create_user_minimal | 2 & 3 | Tests the creation of a user via the OpenStackUser class | ++----------------------------------+---------------+-----------------------------------------------------------+ +| test_create_user_2x | 2 & 3 | Tests the creation of a user a second time via the | +| | | OpenStackUser class to ensure it is only created once | ++----------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_user | 2 & 3 | Tests the creation and deletion of a user via the | +| | | OpenStackUser class to ensure that clean will not raise | +| | | an exception | ++----------------------------------+---------------+-----------------------------------------------------------+ + +create_project_tests.py - CreateProjectUserTests +------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Keystone API | Description | ++=======================================+===============+===========================================================+ +| test_create_project_sec_grp_one_user | 2 & 3 | Tests the creation of an OpenStack object to a project | +| | | with a new users and to create a security group | +| | | | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_project_sec_grp_two_users | 2 & 3 | Tests the creation of an OpenStack object to a project | +| | | with two new users and to create a security group under | +| | | each | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +glance_utils_tests.py - GlanceUtilsTests +------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Glance API | Description | ++=======================================+===============+===========================================================+ +| test_create_image_minimal_url | 1 | Tests the glance_utils.create_image() function with a URL | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_image_minimal_file | 1 | Tests the glance_utils.create_image() function with a file| ++---------------------------------------+---------------+-----------------------------------------------------------+ + +neutron_utils_tests.py - NeutronUtilsNetworkTests +------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++=======================================+===============+===========================================================+ +| test_create_network | 2 | Ensures neutron_utils.create_network() properly creates a | +| | | network | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_network_empty_name | 2 | Ensures neutron_utils.create_network() raises an exception| +| | | when the network name is an empty string | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_network_null_name | 2 | Ensures neutron_utils.create_network() raises an exception| +| | | when the network name is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +neutron_utils_tests.py - NeutronUtilsSubnetTests +------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++=======================================+===============+===========================================================+ +| test_create_subnet | 2 | Ensures neutron_utils.create_subnet() can properly create | +| | | an OpenStack subnet object | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_subnet_null_name | 2 | Ensures neutron_utils.create_subnet() raises an exception | +| | | when the subnet name is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_subnet_empty_name | 2 | Ensures neutron_utils.create_subnet() raises an exception | +| | | when the subnet name is an empty string | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_subnet_null_cidr | 2 | Ensures neutron_utils.create_subnet() raises an exception | +| | | when the subnet CIDR is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_subnet_empty_cidr | 2 | Ensures neutron_utils.create_subnet() raises an exception | +| | | when the subnet CIDR is an empty string | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +neutron_utils_tests.py - NeutronUtilsRouterTests +------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++=======================================+===============+===========================================================+ +| test_create_router_simple | 2 | Ensures neutron_utils.create_router() can properly create | +| | | a simple OpenStack router object | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_with_public_inter | 2 | Ensures neutron_utils.create_router() can properly create | +| face | | an OpenStack router object with an interface to the | +| | | external network | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_empty_name | 2 | Ensures neutron_utils.create_router() raises an exception | +| | | when the name is an empty string | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_null_name | 2 | Ensures neutron_utils.create_router() raises an exception | +| | | when the name is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_add_interface_router | 2 | Ensures neutron_utils.add_interface_router() properly adds| +| | | an interface to another subnet | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_add_interface_router_null_router | 2 | Ensures neutron_utils.add_interface_router() raises an | +| | | exception when the router object is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_add_interface_router_null_subnet | 2 | Ensures neutron_utils.add_interface_router() raises an | +| | | exception when the subnet object is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_port | 2 | Ensures neutron_utils.create_port() can properly create an| +| | | OpenStack port object | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_port_empty_name | 2 | Ensures neutron_utils.create_port() raises an exception | +| | | when the port name is an empty string | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_port_null_name | 2 | Ensures neutron_utils.create_port() raises an exception | +| | | when the port name is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_port_null_network_object | 2 | Ensures neutron_utils.create_port() raises an exception | +| | | when the network object is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_port_null_ip | 2 | Ensures neutron_utils.create_port() raises an exception | +| | | when the assigned IP value is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_port_invalid_ip | 2 | Ensures neutron_utils.create_port() raises an exception | +| | | when the assigned IP value is invalid | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_port_invalid_ip_to_subnet | 2 | Ensures neutron_utils.create_port() raises an exception | +| | | when the assigned IP value is not part of CIDR | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +neutron_utils_tests.py - NeutronUtilsSecurityGroupTests +------------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++=======================================+===============+===========================================================+ +| test_create_delete_simple_sec_grp | 2 | Ensures that a security group can be created | +| | | (neutron_utils.create_security_group() and deleted via | +| | | neutron_utils.delete_security_group() | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_sec_grp_no_name | 2 | Ensures that neutron_utils.create_security_group() raises | +| | | an exception when attempting to create a security group | +| | | without a name | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_sec_grp_no_rules | 2 | Ensures that neutron_utils.create_security_group() can | +| | | create a security group without any rules | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_sec_grp_one_rule | 2 | Ensures that neutron_utils.create_security_group_rule() | +| | | can add a rule to a security group | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +nova_utils_tests.py - NovaUtilsKeypairTests +------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Nova API | Description | ++=======================================+===============+===========================================================+ +| test_create_keypair | 2 | Ensures that a keypair can be properly created via | +| | | nova_utils.upload_keypair() with a public_key object | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_keypair | 2 | Ensures that a keypair can be properly deleted via | +| | | nova_utils.delete_keypair() | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_key_from_file | 2 | Ensures that a keypair can be properly created via | +| | | nova_utils.upload_keypair_file() | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_floating_ips | 2 | Ensures that a floating IP can be properly created via | +| | | nova_utils.create_floating_ip() [note: this test should | +| | | be moved to a new class] | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +nova_utils_tests.py - NovaUtilsFlavorTests +------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Nova API | Description | ++=======================================+===============+===========================================================+ +| test_create_flavor | 2 | Ensures that a flavor can be properly created via | +| | | nova_utils.create_flavor() | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_flavor | 2 | Ensures that a flavor can be properly deleted via | +| | | nova_utils.delete_flavor() | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_flavor_tests.py - CreateFlavorTests +------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Nova API | Description | ++=======================================+===============+===========================================================+ +| test_create_flavor | 2 | Ensures that the OpenStackFlavor class's create() method | +| | | creates an OpenStack flavor object | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_flavor_existing | 2 | Ensures that the OpenStackFlavor class's create() will not| +| | | create a flavor with the same name more than once | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_clean_flavor | 2 | Ensures that the OpenStackFlavor class's clean() method | +| | | will delete the flavor object | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_flavor | 2 | Ensures that the OpenStackFlavor class's clean() method | +| | | will not raise an exception when called and the object no | +| | | longer exists | ++---------------------------------------+---------------+-----------------------------------------------------------+ diff --git a/docs/how-to-use/IntegrationTests.rst b/docs/how-to-use/IntegrationTests.rst new file mode 100644 index 0000000..983c165 --- /dev/null +++ b/docs/how-to-use/IntegrationTests.rst @@ -0,0 +1,290 @@ +SNAPS OpenStack Integration Testing +=================================== + +These tests are ones designed to be run within their own dynamically created project along with a newly generated user +account and generally require other OpenStack object creators. + +The Test Classes +================ + +create_security_group_tests.py - CreateSecurityGroupTests +--------------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_create_group_without_rules | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class can create a | +| | Neutron 2 | security group without any rules | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_group | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class clean() method | +| | Neutron 2 | will not raise an exception should the group be deleted by| +| | | some other process | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_group_with_one_simple_rule| Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class can create a | +| | Neutron 2 | security group with a single rule | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_group_with_several_rules | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class can create a | +| | Neutron 2 | security group with several rules | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_add_rule | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup#add_rule() method | +| | Neutron 2 | properly creates and associates the new rule | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_remove_rule_by_id | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup#remove_rule() method | +| | Neutron 2 | properly deletes and disassociates the old rule via its ID| ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_remove_rule_by_setting | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup#remove_rule() method | +| | Neutron 2 | properly deletes and disassociates the old rule via its | +| | | setting object | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_image_tests.py - CreateImageSuccessTests +----------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Glance API | Description | ++=======================================+===============+===========================================================+ +| test_create_image_clean_url | 1 | Ensures the OpenStackImage class can create an image from | +| | | a download URL location | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_image_clean_file | 1 | Ensures the OpenStackImage class can create an image from | +| | | a locally sourced image file | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_image | 1 | Ensures the OpenStackImage.clean() method deletes an image| +| | | and does not raise an exception on subsequent calls to the| +| | | clean() method | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_same_image | 1 | Ensures the OpenStackImage.create() method does not create| +| | | another image when one already exists with the same name | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_image_tests.py - CreateImageNegativeTests +------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Glance API | Description | ++=======================================+===============+===========================================================+ +| test_none_image_name | 1 | Ensures OpenStackImage.create() results in an Exception | +| | | being raised when the ImageSettings.name attribute has | +| | | not been set | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_bad_image_url | 1 | Ensures OpenStackImage.create() results in an Exception | +| | | being raised when the download URL is invalid | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_bad_image_file | 1 | Ensures OpenStackImage.create() results in an Exception | +| | | being raised when the image file does not exist | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_none_proj_name | 1 | Ensures OpenStackImage.create() results in an Exception | +| | | being raised when the credentials project name is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_none_auth_url | 1 | Ensures OpenStackImage.create() results in an Exception | +| | | being raised when the credentials URL is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_none_password | 1 | Ensures OpenStackImage.create() results in an Exception | +| | | being raised when the credentials password is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_none_user | 1 | Ensures OpenStackImage.create() results in an Exception | +| | | being raised when the credentials user is None | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_keypairs_tests.py - CreateKeypairsTests +---------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Nova API | Description | ++=======================================+===============+===========================================================+ +| test_create_keypair_only | 2 | Ensures that a keypair object can be created simply by | +| | | only configuring a name | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_keypair | 2 | Ensures that a keypair object is deleted via | +| | | OpenStackKeypair.clean() and subsequent calls do not | +| | | result in exceptions | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_keypair_save_pub_only | 2 | Ensures that a keypair object can be created when the only| +| | | the public key is cached to disk | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_keypair_save_both | 2 | Ensures that a keypair object can be created when both the| +| | | public and private keys are cached to disk | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_keypair_from_file | 2 | Ensures that a keypair object can be created with an | +| | | existing public key file | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_network_tests.py - CreateNetworkSuccessTests +--------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++=======================================+===============+===========================================================+ +| test_create_network_without_router | 2 | Ensures that a network can be created via the | +| | | OpenStackNetwork class without any routers | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_network | 2 | Ensures that a router can be deleted via the | +| | | OpenStackNetwork.clean() method | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_network_with_router | 2 | Ensures that a network can be created via the | +| | | OpenStackNetwork class with a router | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_networks_same_name | 2 | Ensures that the OpenStackNetwork.create() method will not| +| | | create a network with the same name | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_router_tests.py - CreateRouterSuccessTests +------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++=======================================+===============+===========================================================+ +| test_create_router_vanilla | 2 | Ensures that a router can be created via the | +| | | OpenStackRouter class with minimal settings | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_delete_router | 2 | Ensures that a router can be deleted via the | +| | | OpenStackRouter.clean() method | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_admin_state_false | 2 | Ensures that a router can created with | +| | | admin_state_up = False | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_admin_state_True | 2 | Ensures that a router can created with | +| | | admin_state_up = True | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_private_network | 2 | Ensures that a router port can be created against a | +| | | private network | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_external_network | 2 | Ensures that a router can be created that is connected to | +| | | both external and private internal networks | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_router_tests.py - CreateRouterNegativeTests +-------------------------------------------------- + ++----------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++========================================+===============+===========================================================+ +| test_create_router_noname | 2 | Ensures that an exception is raised when attempting to | +| | | create a router without a name | ++----------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_router_invalid_gateway_name| 2 | Ensures that an exception is raised when attempting to | +| | | create a router to an external network that does not exist| ++----------------------------------------+---------------+-----------------------------------------------------------+ + +create_instance_tests.py - CreateInstanceSimpleTests +---------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_create_delete_instance | Nova 2 | Ensures that the OpenStackVmInstance.clean() method | +| | Neutron 2 | deletes the instance | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_instance_tests.py - SimpleHealthCheck +-------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_check_vm_ip_dhcp | Nova 2 | Tests the creation of an OpenStack instance with a single | +| | Neutron 2 | port and it's assigned IP address | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_instance_tests.py - CreateInstanceSingleNetworkTests +----------------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_single_port_static | Nova 2 | Ensures that an instance with a single port/NIC with a | +| | Neutron 2 | static IP can be created | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_ssh_client_fip_before_active | Nova 2 | Ensures that an instance can be reached over SSH when the | +| | Neutron 2 | floating IP is assigned prior to the VM becoming ACTIVE | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_ssh_client_fip_after_active | Nova 2 | Ensures that an instance can be reached over SSH when the | +| | Neutron 2 | floating IP is assigned after to the VM becoming ACTIVE | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_instance_tests.py - CreateInstancePortManipulationTests +-------------------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_set_custom_valid_ip_one_subnet | Nova 2 | Ensures that an instance's can have a valid static IP is | +| | Neutron 2 | properly assigned | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_set_custom_invalid_ip_one_subnet | Nova 2 | Ensures that an instance's port with an invalid static IP | +| | Neutron 2 | raises an exception | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_set_custom_valid_mac | Nova 2 | Ensures that an instance's port can have a valid MAC | +| | Neutron 2 | address properly assigned | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_set_custom_invalid_mac | Nova 2 | Ensures that an instance's port with an invalid MAC | +| | Neutron 2 | address raises and exception | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_set_custom_mac_and_ip | Nova 2 | Ensures that an instance's port with a valid static IP and| +| | Neutron 2 | MAC are properly assigned | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_set_allowed_address_pairs | Nova 2 | Ensures the configured allowed_address_pairs is properly | +| | Neutron 2 | set on a VMs port | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_set_allowed_address_pairs_bad_mac| Nova 2 | Ensures the port cannot be created when a bad MAC address | +| | Neutron 2 | format is used in the allowed_address_pairs port attribute| ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_set_allowed_address_pairs_bad_ip | Nova 2 | Ensures the port cannot be created when a bad IP address | +| | Neutron 2 | format is used in the allowed_address_pairs port attribute| ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_instance_tests.py - CreateInstanceOnComputeHost +------------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_deploy_vm_to_each_compute_node | Nova 2 | Tests to ensure that one can fire up an instance on each | +| | Neutron 2 | active compute node | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_instance_tests.py - CreateInstancePubPrivNetTests +-------------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_dual_ports_dhcp | Nova 2 | Ensures that a VM with two ports/NICs can have its second | +| | Neutron 2 | NIC configured via SSH/Ansible after startup | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_instance_tests.py - InstanceSecurityGroupTests +----------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_add_security_group | Nova 2 | Ensures that a VM instance can have security group added | +| | Neutron 2 | to it while its running | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_add_invalid_security_group | Nova 2 | Ensures that a VM instance does not accept the addition of| +| | Neutron 2 | a security group that no longer exists | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_remove_security_group | Nova 2 | Ensures that a VM instance accepts the removal of a | +| | Neutron 2 | security group | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_remove_security_group_never_added| Nova 2 | Ensures that a VM instance does not accept the removal of | +| | Neutron 2 | a security group that was never added in the first place | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_add_same_security_group | Nova 2 | Ensures that a VM instance does not add a security group | +| | Neutron 2 | that has already been added to the instance | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +ansible_utils_tests.py - AnsibleProvisioningTests +------------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API Versions | Description | ++=======================================+===============+===========================================================+ +| test_apply_simple_playbook | Nova 2 | Ensures that an instance assigned with a floating IP will | +| | Neutron 2 | apply a simple Ansible playbook | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_apply_template_playbook | Nova 2 | Ensures that an instance assigned with a floating IP will | +| | Neutron 2 | apply a Ansible playbook containing Jinga2 substitution | +| | | values | ++---------------------------------------+---------------+-----------------------------------------------------------+ diff --git a/docs/how-to-use/Testing.rst b/docs/how-to-use/Testing.rst new file mode 100644 index 0000000..586974a --- /dev/null +++ b/docs/how-to-use/Testing.rst @@ -0,0 +1,44 @@ +Running Unit Test Suite +======================= + +These tests are written in Python and require an that it is setup before running the tests. +See `install directions <index.md>`__ for Python installation instructions. + +Start by cloning the snaps-provisioning repository +-------------------------------------------------- + +``git clone https://gerrit.cablelabs.com/snaps-provisioning`` + +Install Library +--------------- + +``pip install -e <path to repo>/`` + +Execute the tests +----------------- + +| ``cd <path to repo> python snaps/unit_test_suite.py -e [path to RC file] -n [external network name]`` +| \* All Supported Arguments +| \* -e [required - The path to the OpenStack RC file] +| \* -n [required - The name of the external network to use for routers + and floating IPs] +| \* -p [optional - the proxy settings if required. Format : +| \* -s [optional - the proxy command used for SSH connections] +| \* -l [(default INFO) The log level] +| \* -k [optional - When set, tests project and user creation. Use only + if host running tests has access to the cloud's admin network] +| \* -f [optional - When set, will not execute tests requiring Floating + IPS] +| \* -u [optional - When set, the unit tests will be executed] + +Test descriptions +================= + +`Unit Testing <UnitTests.rst>`__ - Tests that do not require a connection to OpenStack +-------------------------------------------------------------------------------------- + +`OpenStack API Tests <APITests.rst>`__ - Tests many individual OpenStack API calls +---------------------------------------------------------------------------------- + +`Integration Tests <IntegrationTests.rst>`__ - Tests OpenStack object creation in a context. These tests will be run within a custom project as a specific user. +---------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/docs/how-to-use/UnitTests.rst b/docs/how-to-use/UnitTests.rst new file mode 100644 index 0000000..efd6426 --- /dev/null +++ b/docs/how-to-use/UnitTests.rst @@ -0,0 +1,91 @@ +SNAPS Unit Testing +================== + +| Tests designated as Unit tests extend the unittest.TestCase class and + can be exercised without any external resources +| other than the filesystem. Most of these tests simply ensure that the + configuration settings classes check their +| constructor arguments properly. + +The Test Classes +================ + +FileUtilsTests +-------------- + +- testFileIsDirectory - ensures that the expected path is a directory +- testFileNotExist - ensures that a file that does not exist returns + False +- testFileExists - ensures that a file that does exist returns True +- testDownloadBadUrl - ensures that an Exception is thrown when + attempting to download a file with a bad URL +- testCirrosImageDownload - ensures that the Cirros image can be + downloaded +- testReadOSEnvFile - ensures that an OpenStack RC file can be properly + parsed + +SecurityGroupRuleSettingsUnitTests +---------------------------------- + +Ensures that all required members are included when constructing a +SecurityGroupRuleSettings object + +SecurityGroupSettingsUnitTests +------------------------------ + +Ensures that all required members are included when constructing a +SecuirtyGroupSettings object + +ImageSettingsUnitTests +---------------------- + +Ensures that all required members are included when constructing a +ImageSettings object + +KeypairSettingsUnitTests +------------------------ + +Ensures that all required members are included when constructing a +KeypairSettings object + +UserSettingsUnitTests +--------------------- + +Ensures that all required members are included when constructing a +UserSettings object + +ProjectSettingsUnitTests +------------------------ + +Ensures that all required members are included when constructing a +ProjectSettings object + +NetworkSettingsUnitTests +------------------------ + +Ensures that all required members are included when constructing a +NetworkSettings object + +SubnetSettingsUnitTests +----------------------- + +Ensures that all required members are included when constructing a +SubnetSettings object + +PortSettingsUnitTests +--------------------- + +Ensures that all required members are included when constructing a +PortSettings object + +FloatingIpSettingsUnitTests +--------------------------- + +Ensures that all required members are included when constructing a +FloatingIpSettings object + +VmInstanceSettingsUnitTests +--------------------------- + +Ensures that all required members are included when constructing a +VmInstanceSettings object diff --git a/docs/how-to-use/VirtEnvDeploy.rst b/docs/how-to-use/VirtEnvDeploy.rst new file mode 100644 index 0000000..7f55f0c --- /dev/null +++ b/docs/how-to-use/VirtEnvDeploy.rst @@ -0,0 +1,275 @@ +Overview +======== + +The main purpose of this project is to enable one to describe a virtual environment in a YAML file and enable the +user to deploy it to an OpenStack cloud in a repeatable manner. There are also options to un-deploy that same +environment by leveraging the original YAML file. + +To deploy/clean virtual environments +==================================== + +- Clone Repository + + - git clone https://gerrit.cablelabs.com/snaps-provisioning + +- Install Library + + - pip install -e / + +- Deploy + + - cd + - python snaps/deploy\_venv.py -e -d + - Working example: + +:: + + python deploy_venv.py -e <path to repo>/examples/complex-network/deploy-complex-network.yaml -d + +- Clean + + - python deploy\_venv.py -e -c + - Working example (cleanup of a previously deployed virtual + environment where the VM has Yardstick installed): + +:: + + python deploy_venv.py -e <path to repo>/examples/complex-network/deploy-complex-network.yaml -c + +Environment Configuration YAML File +=================================== + +The configuration file used to deploy and provision a virtual environment has been designed to describe the required +images, networks, SSH public and private keys, associated VMs, as well as any required post deployment provisioning +tasks. + +\*\*\* Please note that many of the more esoteric optional supported +attributes still have not been fully tested. *** +*** Some of the nested bullets are being hidden by GitLabs, please see +doc/VirtEnvDeploy.md.\*\*\* + +- openstack: the top level tag that denotes configuration for the + OpenStack components + + - connection: - contains the credentials and endpoints required to + connect with OpenStack + - username: - the project's user (required) + - password: - the tentant's user password (required) + - auth\_url: - the URL to the OpenStack APIs (required) + - project\_name: - the name of the OpenStack project for the user + (required) + - http\_proxy: - the {{ host }}:{{ port }} of the proxy server the + HTTPPhotoman01(optional) + - images: - describes each image + - image: + + - name: The unique image name. If the name already exists for + your project, a new one will not be created (required) + - format: The format type of the image i.e. qcow2 (required) + - download\_url: The HTTP download location of the image file + (required) + - nic\_config\_pb\_loc: The file location relative to the CWD + (python directory) to the Ansible Playbook used to configure + VMs with more than one port. VMs get their first NIC configured + for free while subsequent ones are not. This value/script will + only be leveraged when necessary. Centos has been supported + with + "provisioning/ansible/centos-network-setup/configure\_host.yml". + + - networks: + - network: + + - name: The name of the network to be created. If one already + exists, a new one will not be created (required) + - admin\_state\_up: T\|F (default True) + - shared: (optional) + - project\_name: Name of the project who owns the network. Note: + only administrative users can specify projects other than their + own (optional) + - external: T\|F whether or not network is external (default + False) + - network\_type: The type of network to create. (optional) + - subnets: + - subnet: + + - name: The name of the network to be created. If one already + exists, a new one will not be created. Note: although + OpenStack allows for multiple subnets to be applied to any + given network, we have not included support as our current + use cases does not utilize this functionality (required) + - cidr: The subnet mask value (required) + - dns\_nameservers: A list of IP values used for DNS + resolution (default: 8.8.8.8) + - ip\_version: 4\|6 (default: 4) + - project\_name: Name of the project who owns the network. + Note: only administrative users can specify projects other + than their own (optional) + - start: The start address for allocation\_pools (optional) + - end: The ending address for allocation\_pools (optional) + - gateway\_ip: The IP address to the gateway (optional) + - enable\_dhcp: T\|F (optional) + - dns\_nameservers: List of DNS server IPs + - host\_routes: A list of host route dictionaries (optional) + i.e.: + ``yaml "host_routes":[ { "destination":"0.0.0.0/0", "nexthop":"123.456.78.9" }, { "destination":"192.168.0.0/24", "nexthop":"192.168.0.1" } ]`` + - destination: The destination for a static route (optional) + - nexthop: The next hop for the destination (optional) + - ipv6\_ra\_mode: Valid values: "dhcpv6-stateful", + "dhcpv6-stateless", and "slaac" (optional) + - ipv6\_address\_mode: Valid values: "dhcpv6-stateful", + "dhcpv6-stateless", and "slaac" (optional) + + - routers: + + - router: + - name: The name of the router to be created. If one already + exists, a new one will not be created (required) + - project\_name: Name of the project who owns the network. Note: + only administrative users can specify projects other than their + own (optional) + - internal\_subnets: A list of subnet names on which the router + will be placed (optional) + - external\_gateway: A dictionary containing the external gateway + parameters: "network\_id", "enable\_snat", + "external\_fixed\_ips" (optional) + - interfaces: A list of port interfaces to create to other + subnets (optional) + + - port (Leverages the same class/structure as port objects on + VM instances. See port definition below for a + full accounting of the port attributes. The ones listed + below are generally used for routers) + + - name: The name given to the new port (must be unique for + project) (required) + - network\_name: The name of the new port's network + (required) + - ip\_addrs: A list of k/v pairs (optional) + - subnet\_name: the name of a subnet that is on the port's + network + - ip: An IP address of the associated subnet to assign to + the new port (optional but generally required for router + interfaces) + + - keypairs: + + - keypair: + - name: The name of the keypair to be created. If one already + exists, a new one will not be created but simply loaded from + its configured file location (required) + - public\_filepath: The path to where the generated public key + will be stored if it does not exist (optional but really + required for provisioning purposes) + - private\_filepath: The path to where the generated private key + will be stored if it does not exist (optional but really + required for provisioning purposes) + + - instances: + + - instance: + - name: The unique instance name for project. (required) + - flavor: Must be one of the preconfigured flavors (required) + - imageName: The name of the image to be used for deployment + (required) + - keypair\_name: The name of the keypair to attach to instance + (optional but required for NIC configuration and Ansible + provisioning) + - sudo\_user: The name of a sudo\_user that is attached to the + keypair (optional but required for NIC configuration and + Ansible provisioning) + - vm\_boot\_timeout: The number of seconds to block waiting for + an instance to deploy and boot (default 900) + - vm\_delete\_timeout: The number of seconds to block waiting for + an instance to be deleted (default 300) + - ssh\_connect\_timeout: The number of seconds to block waiting + for an instance to achieve an SSH connection (default 120) + - ports: A list of port configurations (should contain at least + one) + - port: Denotes the configuration of a NIC + + - name: The unique port name for project (required) + - network\_name: The name of the network to which the port is + attached (required) + - ip\_addrs: Static IP addresses to be added to the port by + subnet (optional) + - subnet\_name: The name of the subnet + - ip: The assigned IP address (when null, OpenStack will + assign an IP to the port) + - admin\_state\_up: T\|F (default True) + - project\_name: The name of the project who owns the network. + Only administrative users can specify a the project ID other + than their own (optional) + - mac\_address: The desired MAC for the port (optional) + - fixed\_ips: A dictionary that allows one to specify only a + subnet ID, OpenStack Networking allocates an available IP + from that subnet to the port. If you specify both a subnet + ID and an IP address, OpenStack Networking tries to allocate + the specified address to the port. (optional) + - seurity\_groups: A list of security group IDs (optional) + - allowed\_address\_pairs: A dictionary containing a set of + zero or more allowed address pairs. An address pair contains + an IP address and MAC address. (optional) + - opt\_value: The extra DHCP option value (optional) + - opt\_name: The extra DHCP option name (optional) + - device\_owner: The ID of the entity that uses this port. For + example, a DHCP agent (optional) + - device\_id: The ID of the device that uses this port. For + example, a virtual server (optional) + + - floating\_ips: list of floating\_ip configurations (optional) + + - floating\_ip: + - name: Must be unique for VM instance (required) + - port\_name: The name of the port requiring access to the + external network (required) + - subnet\_name: The name of the subnet contains the IP address on + the port on which to create the floating IP (optional) + - router\_name: The name of the router connected to an external + network used to attach the floating IP (required) + - provisioning: (True\|False) Denotes whether or not this IP can + be used for Ansible provisioning (default True) + +- ansible: Each set of attributes below are contained in a list + + - playbook\_location: Full path or relative to the directory in + which the deployment file resides (required) + - hosts: A list of hosts to which the playbook will be executed + (required) + - variables: Should your Ansible scripts require any substitution + values to be applied with Jinga2templates, the values defined here + will be used to for substitution + - tag name = substitution variable names. For instance, for any file + being pushed to the host being provisioned containing a value such + as {{ foo }}, you must specify a tag name of "foo" + + - vm\_name: + - type: string\|port\|os\_creds\|vm-attr (note: will need to make + changes to deploy\_venv.py#\_\_get\_variable\_value() for + additional support) + - when type == string, an tag name "value" must exist and its + value will be used for template substituion + - when type == port, custom code has been written to extract + certain assigned values to the port: + + - vm\_name: must correspond to a VM's name as configured in + this file + - port\_name: The name of the port from which to extract the + substitution values (required) + - port\_value: The port value. Currently only supporting + "mac\_address" and "ip\_address" (only the first) + + - when type == os\_creds, custom code has been written to extract + the file's connection values: + + - username: connection's user + - password: connection's password + - auth\_url: connection's URL + - project\_name: connection's project + + - when type == vm-attr, custom code has been written to extract + the following attributes from the vm: + + - vm\_name: must correspond to a VM's name as configured in + this file + - value -> floating\_ip: is currently the only vm-attr + supported diff --git a/docs/how-to-use/index.rst b/docs/how-to-use/index.rst new file mode 100644 index 0000000..58b67a3 --- /dev/null +++ b/docs/how-to-use/index.rst @@ -0,0 +1,26 @@ +************************* +Runtime Environment Setup +************************* + +- Python 2.7 (recommend leveraging a Virtual Python runtime, e.g. + `Virtualenv <https://virtualenv.pypa.io>`__, in your development + environment) +- Development packages for python and openssl. On CentOS/RHEL: + + # yum install python-devel openssl-devel + +On Ubuntu: + +:: + + # apt-get install python2.7-dev libssl-dev + +- Install SNAPS Library + + - pip install -e <path to repo>/snaps/ + +`Testing <Testing.rst>`__ +------------------------- + +`Virtual Environment Deployment <VirtEnvDeploy.rst>`__ +------------------------------------------------------ diff --git a/examples/complex-network/deploy-complex-network.yaml b/examples/complex-network/deploy-complex-network.yaml new file mode 100644 index 0000000..42559e8 --- /dev/null +++ b/examples/complex-network/deploy-complex-network.yaml @@ -0,0 +1,234 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +openstack: + connection: + # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host. + username: admin + password: cable123 + auth_url: http://10.197.103.50:5000/v2.0/ + project_name: admin + http_proxy: localhost:3128 + images: + - image: + name: centos-inst-test + format: qcow2 + image_user: centos + download_url: http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2 + nic_config_pb_loc: provisioning/ansible/centos-network-setup/playbooks/configure_host.yml + - image: + name: Ubuntu-14.04 + format: qcow2 + image_user: ubuntu + download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img + nic_config_pb_loc: provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml + networks: + - network: + name: mgr-net + subnets: + - subnet: + name: mgr-subnet + cidr: 10.0.1.0/24 + dns_nameservers: [8.8.8.8] + - subnet: + name: mgr-subnet-2 + cidr: 10.0.2.0/24 + dns_nameservers: [8.8.8.8] + - network: + name: site1-net + subnets: + - subnet: + name: site1-subnet + cidr: 192.168.0.0/24 + gateway_ip: 192.168.0.1 + - subnet: + name: site1-subnet-2 + cidr: 192.168.1.0/24 + gateway_ip: 192.168.1.1 + - network: + name: site2-net + subnets: + - subnet: + name: site2-subnet + cidr: 192.169.0.0/24 + gateway_ip: 192.169.0.1 + routers: + # Note: Routers between internal networks not being used but put in here as an example on how to do that. + - router: + name: mgr-router + external_gateway: external + internal_subnets: + - mgr-subnet + - mgr-subnet-2 + interfaces: + - port: + name: mgr-router-to-site1 + network_name: site1-net + ip_addrs: + - subnet_name: site1-subnet + ip: 192.168.0.10 + - router: + name: site1-router + external_gateway: external + internal_subnets: + - site1-subnet + - router: + name: site2-router + external_gateway: external + internal_subnets: + - site2-subnet + - router: + name: site-to-site-router + interfaces: + - port: + name: site1-router-port + network_name: site1-net + ip_addrs: + - subnet_name: site1-subnet + ip: 192.168.0.100 + - port: + name: site2-router-port + network_name: site2-net + ip_addrs: + - subnet_name: site2-subnet + ip: 192.169.0.100 + keypairs: + - keypair: + name: cmplx-net-kp + public_filepath: /tmp/cmplx-net-kp.pub + private_filepath: /tmp/cmplx-net-kp + instances: + - instance: + name: mgr-app + flavor: m1.small + imageName: centos-inst-test + keypair_name: cmplx-net-kp + vm_boot_timeout: 600 + vm_delete_timeout: 120 + ssh_connect_timeout: 120 + ports: + - port: + name: mgr-app-port + network_name: mgr-net + ip_addrs: + - subnet_name: mgr-subnet + ip: 10.0.1.30 + - subnet_name: mgr-subnet-2 + ip: 10.0.2.30 + floating_ips: +# TODO - Why is only one of these floating IPs not working and why does it vary which one? +# - floating_ip: +# name: fip1 +# port_name: mgr-app-port +# subnet_name: mgr-subnet +# router_name: mgr-router +# provisioning: False + - floating_ip: + name: fip2 + port_name: mgr-app-port + subnet_name: mgr-subnet-2 + router_name: mgr-router + - instance: + name: site1-ovs + flavor: m1.small + imageName: centos-inst-test + keypair_name: cmplx-net-kp + vm_boot_timeout: 600 + vm_delete_timeout: 120 + ssh_connect_timeout: 120 + ports: + - port: + name: site1-ovs-mgr-port + network_name: mgr-net + - port: + name: site1-ovs-site1-port + network_name: site1-net + floating_ips: + - floating_ip: + name: fip1 + port_name: site1-ovs-mgr-port + router_name: mgr-router + - instance: + name: site2-ovs + flavor: m1.small + imageName: Ubuntu-14.04 + keypair_name: cmplx-net-kp + vm_boot_timeout: 600 + vm_delete_timeout: 120 + ssh_connect_timeout: 120 + ports: + - port: + name: site2-ovs-mgr-port + network_name: mgr-net + - port: + name: site2-ovs-site2-port + network_name: site2-net + floating_ips: + - floating_ip: + name: fip1 + port_name: site2-ovs-mgr-port + subnet_name: mgr-subnet-2 + router_name: mgr-router + - instance: + name: site2-host + flavor: m1.small + imageName: Ubuntu-14.04 + keypair_name: cmplx-net-kp + vm_boot_timeout: 600 + vm_delete_timeout: 120 + ssh_connect_timeout: 120 + ports: + - port: + name: site2-host-port + network_name: site2-net + floating_ips: + - floating_ip: + name: fip1 + port_name: site2-host-port + subnet_name: site2-subnet + router_name: site2-router +# TODO - Add a playbook here... +#ansible: +# - playbook_location: main.yml +# hosts: +# - mgr-app +# - site1-ovs +# - site2-ovs +# - site2-host +# variables: +# mac1: +# type: port +# vm_name: site1-ovs +# port_name: site1-ovs-site1-port +# port_value: mac_address +# ip1: +# type: port +# vm_name: site1-ovs +# port_name: site1-ovs-mgr-port +# port_value: ip_address +# mac2: +# type: port +# vm_name: site2-ovs +# port_name: site2-ovs-site2-port +# port_value: mac_address +# ip2: +# type: port +# vm_name: site2-ovs +# port_name: site2-ovs-mgr-port +# port_value: ip_address +# - playbook_location: ./main.yml +# hosts: +# - site1-ovs +# - site2-ovs diff --git a/examples/complex-network/main.yml b/examples/complex-network/main.yml new file mode 100644 index 0000000..7f213ea --- /dev/null +++ b/examples/complex-network/main.yml @@ -0,0 +1,16 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- include: playbooks/sample-playbook.yml
\ No newline at end of file diff --git a/examples/complex-network/playbooks/sample-playbook.yml b/examples/complex-network/playbooks/sample-playbook.yml new file mode 100644 index 0000000..726f213 --- /dev/null +++ b/examples/complex-network/playbooks/sample-playbook.yml @@ -0,0 +1,20 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- hosts: all + + tasks: + - name: Say hello + command: echo 'hello world' > ~/hello.out diff --git a/examples/external-network/deploy-ext-net.yaml b/examples/external-network/deploy-ext-net.yaml new file mode 100644 index 0000000..31c41ec --- /dev/null +++ b/examples/external-network/deploy-ext-net.yaml @@ -0,0 +1,77 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +openstack: + connection: + # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host. + username: admin + password: cable123 + auth_url: http://10.197.103.50:5000/v2.0/ + project_name: admin + http_proxy: localhost:3128 + images: + - image: + name: Ubuntu + format: qcow2 + image_user: ubuntu + download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img + networks: + - network: + name: ext-net + external: True + network_type: vlan + project_name: service + subnets: + - subnet: + name: ext-subnet + cidr: 10.197.101.0/24 + gateway_ip: 10.197.101.1 + start: 10.197.101.101 + end: 10.197.101.200 + - network: + name: internal-net + subnets: + - subnet: + name: internal-subnet + cidr: 10.0.1.0/24 + dns_nameservers: [8.8.8.8] + routers: + - router: + name: ext-net-router + external_gateway: ext-net + internal_subnets: + - internal-subnet + keypairs: + - keypair: + name: ext-net-kp + public_filepath: /tmp/ext-net.pub + private_filepath: /tmp/ext-net + instances: + - instance: + name: ext-net-app + flavor: m1.small + imageName: Ubuntu + keypair_name: ext-net-kp + ports: + - port: + name: internal-net-port + network_name: internal-net + floating_ips: + - floating_ip: + name: fip1 + port_name: internal-net-port + router_name: ext-net-router + subnet_name: internal-subnet + diff --git a/examples/simple/deploy-simple.yaml b/examples/simple/deploy-simple.yaml new file mode 100644 index 0000000..ae946de --- /dev/null +++ b/examples/simple/deploy-simple.yaml @@ -0,0 +1,101 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +openstack: + connection: + # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host. + username: admin + password: cable123 +# auth_url: http://10.197.103.50:5000/v2.0/ + auth_url: http://192.168.67.10:5000/v2.0 + project_name: admin + http_proxy: 10.197.123.27:3128 + ssh_proxy_cmd: '/usr/local/bin/corkscrew 10.197.123.27 3128 %h %p' + images: + - image: + name: Ubuntu14 + format: qcow2 + image_user: ubuntu + download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img + networks: + - network: + name: simple-net + subnets: + - subnet: + name: simple-subnet + cidr: 10.0.1.0/24 + dns_nameservers: [10.5.0.8, 8.8.8.8] + routers: + - router: + name: simple-router + external_gateway: external + internal_subnets: + - simple-subnet + keypairs: + - keypair: + name: simple + public_filepath: /tmp/simple.pub + private_filepath: /tmp/simple + instances: + - instance: + name: simple-1 + flavor: m1.small + imageName: Ubuntu14 + keypair_name: simple + userdata: "#cloud-config\npassword: cable123\nchpasswd: { expire: False }\nsshr_pwauth: True" + ports: + - port: + name: simple-net-port + network_name: simple-net + floating_ips: + - floating_ip: + name: fip1 + port_name: simple-net-port + router_name: simple-router + subnet_name: simple-subnet +ansible: + - playbook_location: main.yml + hosts: + - simple-1 + variables: + greeting_msg: + type: string + value: Greetings + os_user: + type: os_creds + value: username + os_pass: + type: os_creds + value: password + os_auth_url: + type: os_creds + value: auth_url + os_project: + type: os_creds + value: project_name + fip1: + type: vm-attr + vm_name: simple-1 + value: floating_ip + mac1: + type: port + vm_name: simple-1 + port_name: simple-net-port + port_value: mac_address + ip1: + type: port + vm_name: simple-1 + port_name: simple-net-port + port_value: ip_address
\ No newline at end of file diff --git a/examples/simple/files/motd b/examples/simple/files/motd new file mode 100644 index 0000000..cee5d06 --- /dev/null +++ b/examples/simple/files/motd @@ -0,0 +1,8 @@ +{{ greeting_msg }} +OS_USER - {{ os_user }} +OS_PASSWORD - {{ os_pass }} +AUTH_URL - {{ os_auth_url }} +PROJECT_NAME - {{ os_project }} +Floating IP - {{ fip1 }} +Port MAC = {{ mac1 }} +Port IP = {{ ip1 }} diff --git a/examples/simple/main.yml b/examples/simple/main.yml new file mode 100644 index 0000000..7f213ea --- /dev/null +++ b/examples/simple/main.yml @@ -0,0 +1,16 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- include: playbooks/sample-playbook.yml
\ No newline at end of file diff --git a/examples/simple/playbooks/sample-playbook.yml b/examples/simple/playbooks/sample-playbook.yml new file mode 100644 index 0000000..84c46e4 --- /dev/null +++ b/examples/simple/playbooks/sample-playbook.yml @@ -0,0 +1,23 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- hosts: all + become: yes + become_method: sudo + become_user: root + + tasks: + - name: Create MOTD + action: template owner=root group=root mode=666 src=../files/motd dest=/etc/motd diff --git a/examples/two-network/deploy-two-net-centos.yaml b/examples/two-network/deploy-two-net-centos.yaml new file mode 100644 index 0000000..4fae4aa --- /dev/null +++ b/examples/two-network/deploy-two-net-centos.yaml @@ -0,0 +1,96 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +openstack: + connection: + # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host. + username: admin + password: cable123 + auth_url: http://10.197.103.50:5000/v2.0/ + project_name: admin + http_proxy: localhost:3128 + images: + - image: + name: centos + format: qcow2 + image_user: centos + download_url: http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2 + nic_config_pb_loc: provisioning/ansible/centos-network-setup/playbooks/configure_host.yml + networks: + - network: + name: net-1 + subnets: + - subnet: + name: subnet-1 + cidr: 10.0.1.0/24 + dns_nameservers: [8.8.8.8] + - network: + name: net-2 + subnets: + - subnet: + name: subnet-2 + cidr: 10.0.2.0/24 + dns_nameservers: [8.8.8.8] + routers: + - router: + name: router-1 + external_gateway: external + internal_subnets: + - subnet-1 + keypairs: + - keypair: + name: two-net + public_filepath: /tmp/two-net.pub + private_filepath: /tmp/two-net + instances: + - instance: + name: vm1 + flavor: m1.small + imageName: centos + keypair_name: two-net + ports: + - port: + name: port-1-vm1 + network_name: net-1 + - port: + name: port-2-vm1 + network_name: net-2 + floating_ips: + - floating_ip: + name: fip1 + port_name: port-1-vm1 + router_name: router-1 + subnet_name: subnet-1 + - instance: + name: vm2 + flavor: m1.small + imageName: centos + keypair_name: two-net + ports: + - port: + name: port-1-vm2 + network_name: net-1 + - port: + name: port-2-vm2 + network_name: net-2 + ip_addrs: + - subnet_name: subnet-2 + ip: 10.0.2.101 + floating_ips: + - floating_ip: + name: fip1 + port_name: port-1-vm2 + router_name: router-1 + subnet_name: subnet-1
\ No newline at end of file diff --git a/examples/two-network/deploy-two-net-ubuntu.yaml b/examples/two-network/deploy-two-net-ubuntu.yaml new file mode 100644 index 0000000..ffcb05d --- /dev/null +++ b/examples/two-network/deploy-two-net-ubuntu.yaml @@ -0,0 +1,96 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +openstack: + connection: + # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host. + username: admin + password: cable123 + auth_url: http://10.197.103.50:5000/v2.0/ + project_name: admin + http_proxy: localhost:3128 + images: + - image: + name: Ubuntu + format: qcow2 + image_user: ubuntu + download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img + nic_config_pb_loc: provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml + networks: + - network: + name: net-1 + subnets: + - subnet: + name: subnet-1 + cidr: 10.0.1.0/24 + dns_nameservers: [8.8.8.8] + - network: + name: net-2 + subnets: + - subnet: + name: subnet-2 + cidr: 10.0.2.0/24 + dns_nameservers: [8.8.8.8] + routers: + - router: + name: router-1 + external_gateway: external + internal_subnets: + - subnet-1 + keypairs: + - keypair: + name: simple + public_filepath: /tmp/simple.pub + private_filepath: /tmp/simple + instances: + - instance: + name: vm1 + flavor: m1.small + imageName: Ubuntu + keypair_name: simple + ports: + - port: + name: port-1-vm1 + network_name: net-1 + - port: + name: port-2-vm1 + network_name: net-2 + floating_ips: + - floating_ip: + name: fip1 + port_name: port-1-vm1 + router_name: router-1 + subnet_name: subnet-1 + - instance: + name: vm2 + flavor: m1.small + imageName: Ubuntu + keypair_name: simple + ports: + - port: + name: port-1-vm2 + network_name: net-1 + - port: + name: port-2-vm2 + network_name: net-2 + ip_addrs: + - subnet_name: subnet-2 + ip: 10.0.2.101 + floating_ips: + - floating_ip: + name: fip1 + port_name: port-1-vm2 + router_name: router-1 + subnet_name: subnet-1
\ No newline at end of file diff --git a/snaps/__init__.py b/snaps/__init__.py new file mode 100644 index 0000000..e3e876e --- /dev/null +++ b/snaps/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' diff --git a/snaps/deploy_venv.py b/snaps/deploy_venv.py new file mode 100644 index 0000000..10e444f --- /dev/null +++ b/snaps/deploy_venv.py @@ -0,0 +1,573 @@ +#!/usr/bin/python +# +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This script is responsible for deploying virtual environments +import argparse +import logging +import os +import re + +from snaps import file_utils +from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor +from snaps.openstack.create_image import ImageSettings +from snaps.openstack.create_instance import VmInstanceSettings +from snaps.openstack.create_network import PortSettings, NetworkSettings +from snaps.openstack.create_router import RouterSettings +from snaps.openstack.create_keypairs import KeypairSettings +from snaps.openstack.os_credentials import OSCreds, ProxySettings +from snaps.openstack.utils import deploy_utils +from snaps.provisioning import ansible_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('deploy_venv') + +ARG_NOT_SET = "argument not set" + + +def __get_os_credentials(os_conn_config): + """ + Returns an object containing all of the information required to access OpenStack APIs + :param os_conn_config: The configuration holding the credentials + :return: an OSCreds instance + """ + proxy_settings = None + http_proxy = os_conn_config.get('http_proxy') + if http_proxy: + tokens = re.split(':', http_proxy) + ssh_proxy_cmd = os_conn_config.get('ssh_proxy_cmd') + proxy_settings = ProxySettings(tokens[0], tokens[1], ssh_proxy_cmd) + + return OSCreds(username=os_conn_config.get('username'), + password=os_conn_config.get('password'), + auth_url=os_conn_config.get('auth_url'), + project_name=os_conn_config.get('project_name'), + proxy_settings=proxy_settings) + + +def __parse_ports_config(config): + """ + Parses the "ports" configuration + :param config: The dictionary to parse + :param os_creds: The OpenStack credentials object + :return: a list of PortConfig objects + """ + out = list() + for port_config in config: + out.append(PortSettings(config=port_config.get('port'))) + return out + + +def __create_flavors(os_conn_config, flavors_config, cleanup=False): + """ + Returns a dictionary of flavors where the key is the image name and the value is the image object + :param os_conn_config: The OpenStack connection credentials + :param flavors_config: The list of image configurations + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: dictionary + """ + flavors = {} + + if flavors_config: + try: + for flavor_config_dict in flavors_config: + flavor_config = flavor_config_dict.get('flavor') + if flavor_config and flavor_config.get('name'): + flavor_creator = OpenStackFlavor(__get_os_credentials(os_conn_config), + FlavorSettings(flavor_config)) + flavor_creator.create(cleanup=cleanup) + flavors[flavor_config['name']] = flavor_creator + except Exception as e: + for key, flavor_creator in flavors.iteritems(): + flavor_creator.clean() + raise e + logger.info('Created configured flavors') + + return flavors + + +def __create_images(os_conn_config, images_config, cleanup=False): + """ + Returns a dictionary of images where the key is the image name and the value is the image object + :param os_conn_config: The OpenStack connection credentials + :param images_config: The list of image configurations + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: dictionary + """ + images = {} + + if images_config: + try: + for image_config_dict in images_config: + image_config = image_config_dict.get('image') + if image_config and image_config.get('name'): + images[image_config['name']] = deploy_utils.create_image(__get_os_credentials(os_conn_config), + ImageSettings(image_config), cleanup) + except Exception as e: + for key, image_creator in images.iteritems(): + image_creator.clean() + raise e + logger.info('Created configured images') + + return images + + +def __create_networks(os_conn_config, network_confs, cleanup=False): + """ + Returns a dictionary of networks where the key is the network name and the value is the network object + :param os_conn_config: The OpenStack connection credentials + :param network_confs: The list of network configurations + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: dictionary + """ + network_dict = {} + + if network_confs: + try: + for network_conf in network_confs: + net_name = network_conf['network']['name'] + os_creds = __get_os_credentials(os_conn_config) + network_dict[net_name] = deploy_utils.create_network( + os_creds, NetworkSettings(config=network_conf['network']), cleanup) + except Exception as e: + for key, net_creator in network_dict.iteritems(): + net_creator.clean() + raise e + + logger.info('Created configured networks') + + return network_dict + + +def __create_routers(os_conn_config, router_confs, cleanup=False): + """ + Returns a dictionary of networks where the key is the network name and the value is the network object + :param os_conn_config: The OpenStack connection credentials + :param router_confs: The list of router configurations + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: dictionary + """ + router_dict = {} + os_creds = __get_os_credentials(os_conn_config) + + if router_confs: + try: + for router_conf in router_confs: + router_name = router_conf['router']['name'] + router_dict[router_name] = deploy_utils.create_router( + os_creds, RouterSettings(config=router_conf['router']), cleanup) + except Exception as e: + for key, router_creator in router_dict.iteritems(): + router_creator.clean() + raise e + + logger.info('Created configured networks') + + return router_dict + + +def __create_keypairs(os_conn_config, keypair_confs, cleanup=False): + """ + Returns a dictionary of keypairs where the key is the keypair name and the value is the keypair object + :param os_conn_config: The OpenStack connection credentials + :param keypair_confs: The list of keypair configurations + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: dictionary + """ + keypairs_dict = {} + if keypair_confs: + try: + for keypair_dict in keypair_confs: + keypair_config = keypair_dict['keypair'] + kp_settings = KeypairSettings(keypair_config) + keypairs_dict[keypair_config['name']] = deploy_utils.create_keypair( + __get_os_credentials(os_conn_config), kp_settings, cleanup) + except Exception as e: + for key, keypair_creator in keypairs_dict.iteritems(): + keypair_creator.clean() + raise e + + logger.info('Created configured keypairs') + + return keypairs_dict + + +def __create_instances(os_conn_config, instances_config, image_dict, keypairs_dict, cleanup=False): + """ + Returns a dictionary of instances where the key is the instance name and the value is the VM object + :param os_conn_config: The OpenStack connection credentials + :param instances_config: The list of VM instance configurations + :param image_dict: A dictionary of images that will probably be used to instantiate the VM instance + :param keypairs_dict: A dictionary of keypairs that will probably be used to instantiate the VM instance + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: dictionary + """ + os_creds = __get_os_credentials(os_conn_config) + + vm_dict = {} + + if instances_config: + try: + for instance_config in instances_config: + conf = instance_config.get('instance') + if conf: + if image_dict: + image_creator = image_dict.get(conf.get('imageName')) + if image_creator: + instance_settings = VmInstanceSettings(config=instance_config['instance']) + kp_name = conf.get('keypair_name') + vm_dict[conf['name']] = deploy_utils.create_vm_instance( + os_creds, instance_settings, image_creator.image_settings, + keypair_creator=keypairs_dict[kp_name], cleanup=cleanup) + else: + raise Exception('Image creator instance not found. Cannot instantiate') + else: + raise Exception('Image dictionary is None. Cannot instantiate') + else: + raise Exception('Instance configuration is None. Cannot instantiate') + except Exception as e: + logger.error('Unexpected error creating instances. Attempting to cleanup environment - ' + e.message) + for key, inst_creator in vm_dict.iteritems(): + inst_creator.clean() + raise e + + logger.info('Created configured instances') + + return vm_dict + + +def __apply_ansible_playbooks(ansible_configs, vm_dict, env_file): + """ + Applies ansible playbooks to running VMs with floating IPs + :param ansible_configs: a list of Ansible configurations + :param vm_dict: the dictionary of newly instantiated VMs where the VM name is the key + :param env_file: the path of the environment for setting the CWD so playbook location is relative to the deployment + file + :return: t/f - true if successful + """ + logger.info("Applying Ansible Playbooks") + if ansible_configs: + # Ensure all hosts are accepting SSH session requests + for vm_inst in vm_dict.values(): + if not vm_inst.vm_ssh_active(block=True): + logger.warn("Timeout waiting for instance to respond to SSH requests") + return False + + # Set CWD so the deployment file's playbook location can leverage relative paths + orig_cwd = os.getcwd() + env_dir = os.path.dirname(env_file) + os.chdir(env_dir) + + # Apply playbooks + for ansible_config in ansible_configs: + __apply_ansible_playbook(ansible_config, vm_dict) + + # Return to original directory + os.chdir(orig_cwd) + + return True + + +def __apply_ansible_playbook(ansible_config, vm_dict): + """ + Applies an Ansible configuration setting + :param ansible_config: the configuration settings + :param vm_dict: the dictionary of newly instantiated VMs where the VM name is the key + :return: + """ + if ansible_config: + remote_user, floating_ips, private_key_filepath, proxy_settings = __get_connection_info(ansible_config, vm_dict) + if floating_ips: + ansible_utils.apply_playbook(ansible_config['playbook_location'], floating_ips, remote_user, + private_key_filepath, + variables=__get_variables(ansible_config.get('variables'), vm_dict), + proxy_setting=proxy_settings) + + +def __get_connection_info(ansible_config, vm_dict): + """ + Returns a tuple of data required for connecting to the running VMs + (remote_user, [floating_ips], private_key_filepath, proxy_settings) + :param ansible_config: the configuration settings + :param vm_dict: the dictionary of VMs where the VM name is the key + :return: tuple where the first element is the user and the second is a list of floating IPs and the third is the + private key file location and the fourth is an instance of the snaps.ProxySettings class + (note: in order to work, each of the hosts need to have the same sudo_user and private key file location values) + """ + if ansible_config.get('hosts'): + hosts = ansible_config['hosts'] + if len(hosts) > 0: + floating_ips = list() + remote_user = None + private_key_filepath = None + proxy_settings = None + for host in hosts: + vm = vm_dict.get(host) + if vm: + fip = vm.get_floating_ip() + if fip: + remote_user = vm.get_image_user() + + if fip: + floating_ips.append(fip.ip) + else: + raise Exception('Could not find floating IP for VM - ' + vm.name) + + private_key_filepath = vm.keypair_settings.private_filepath + proxy_settings = vm.get_os_creds().proxy_settings + else: + logger.error('Could not locate VM with name - ' + host) + + return remote_user, floating_ips, private_key_filepath, proxy_settings + return None + + +def __get_variables(var_config, vm_dict): + """ + Returns a dictionary of substitution variables to be used for Ansible templates + :param var_config: the variable configuration settings + :param vm_dict: the dictionary of VMs where the VM name is the key + :return: dictionary or None + """ + if var_config and vm_dict and len(vm_dict) > 0: + variables = dict() + for key, value in var_config.iteritems(): + value = __get_variable_value(value, vm_dict) + if key and value: + variables[key] = value + logger.info("Set Jinga2 variable with key [" + key + "] the value [" + value + ']') + else: + logger.warn('Key [' + str(key) + '] or Value [' + str(value) + '] must not be None') + return variables + return None + + +def __get_variable_value(var_config_values, vm_dict): + """ + Returns the associated variable value for use by Ansible for substitution purposes + :param var_config_values: the configuration dictionary + :param vm_dict: the dictionary containing all VMs where the key is the VM's name + :return: + """ + if var_config_values['type'] == 'string': + return __get_string_variable_value(var_config_values) + if var_config_values['type'] == 'vm-attr': + return __get_vm_attr_variable_value(var_config_values, vm_dict) + if var_config_values['type'] == 'os_creds': + return __get_os_creds_variable_value(var_config_values, vm_dict) + if var_config_values['type'] == 'port': + return __get_vm_port_variable_value(var_config_values, vm_dict) + return None + + +def __get_string_variable_value(var_config_values): + """ + Returns the associated string value + :param var_config_values: the configuration dictionary + :return: the value contained in the dictionary with the key 'value' + """ + return var_config_values['value'] + + +def __get_vm_attr_variable_value(var_config_values, vm_dict): + """ + Returns the associated value contained on a VM instance + :param var_config_values: the configuration dictionary + :param vm_dict: the dictionary containing all VMs where the key is the VM's name + :return: the value + """ + vm = vm_dict.get(var_config_values['vm_name']) + if vm: + if var_config_values['value'] == 'floating_ip': + return vm.get_floating_ip().ip + + +def __get_os_creds_variable_value(var_config_values, vm_dict): + """ + Returns the associated OS credentials value + :param var_config_values: the configuration dictionary + :param vm_dict: the dictionary containing all VMs where the key is the VM's name + :return: the value + """ + logger.info("Retrieving OS Credentials") + vm = vm_dict.values()[0] + + if vm: + if var_config_values['value'] == 'username': + logger.info("Returning OS username") + return vm.get_os_creds().username + elif var_config_values['value'] == 'password': + logger.info("Returning OS password") + return vm.get_os_creds().password + elif var_config_values['value'] == 'auth_url': + logger.info("Returning OS auth_url") + return vm.get_os_creds().auth_url + elif var_config_values['value'] == 'project_name': + logger.info("Returning OS project_name") + return vm.get_os_creds().project_name + + logger.info("Returning none") + return None + + +def __get_vm_port_variable_value(var_config_values, vm_dict): + """ + Returns the associated OS credentials value + :param var_config_values: the configuration dictionary + :param vm_dict: the dictionary containing all VMs where the key is the VM's name + :return: the value + """ + port_name = var_config_values.get('port_name') + vm_name = var_config_values.get('vm_name') + + if port_name and vm_name: + vm = vm_dict.get(vm_name) + if vm: + port_value_id = var_config_values.get('port_value') + if port_value_id: + if port_value_id == 'mac_address': + return vm.get_port_mac(port_name) + if port_value_id == 'ip_address': + return vm.get_port_ip(port_name) + + +def main(arguments): + """ + Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or ... + Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg containing the following content: + + [defaults] + host_key_checking = False + + CWD must be this directory where this script is located. + + :return: To the OS + """ + log_level = logging.INFO + if arguments.log_level != 'INFO': + log_level = logging.DEBUG + logging.basicConfig(level=log_level) + + logger.info('Starting to Deploy') + config = file_utils.read_yaml(arguments.environment) + logger.info('Read configuration file - ' + arguments.environment) + + if config: + os_config = config.get('openstack') + + image_dict = {} + network_dict = {} + router_dict = {} + keypairs_dict = {} + vm_dict = {} + + if os_config: + try: + os_conn_config = os_config.get('connection') + + # Create flavors + flavor_dict = __create_flavors(os_conn_config, os_config.get('flavors'), + arguments.clean is not ARG_NOT_SET) + + # Create images + image_dict = __create_images(os_conn_config, os_config.get('images'), + arguments.clean is not ARG_NOT_SET) + + # Create network + network_dict = __create_networks(os_conn_config, os_config.get('networks'), + arguments.clean is not ARG_NOT_SET) + + # Create network + router_dict = __create_routers(os_conn_config, os_config.get('routers'), + arguments.clean is not ARG_NOT_SET) + + # Create keypairs + keypairs_dict = __create_keypairs(os_conn_config, os_config.get('keypairs'), + arguments.clean is not ARG_NOT_SET) + + # Create instance + vm_dict = __create_instances(os_conn_config, os_config.get('instances'), image_dict, keypairs_dict, + arguments.clean is not ARG_NOT_SET) + logger.info('Completed creating/retrieving all configured instances') + except Exception as e: + logger.error('Unexpected error deploying environment. Rolling back due to - ' + e.message) + __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict, True) + raise e + + + # Must enter either block + if arguments.clean is not ARG_NOT_SET: + # Cleanup Environment + __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict, + arguments.clean_image is not ARG_NOT_SET) + elif arguments.deploy is not ARG_NOT_SET: + logger.info('Configuring NICs where required') + for vm in vm_dict.itervalues(): + vm.config_nics() + logger.info('Completed NIC configuration') + + # Provision VMs + ansible_config = config.get('ansible') + if ansible_config and vm_dict: + if not __apply_ansible_playbooks(ansible_config, vm_dict, arguments.environment): + logger.error("Problem applying ansible playbooks") + else: + logger.error('Unable to read configuration file - ' + arguments.environment) + exit(1) + + exit(0) + + +def __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict, clean_image=False): + for key, vm_inst in vm_dict.iteritems(): + vm_inst.clean() + for key, kp_inst in keypairs_dict.iteritems(): + kp_inst.clean() + for key, router_inst in router_dict.iteritems(): + router_inst.clean() + for key, net_inst in network_dict.iteritems(): + net_inst.clean() + if clean_image: + for key, image_inst in image_dict.iteritems(): + image_inst.clean() + for key, flavor_inst in flavor_dict.iteritems(): + flavor_inst.clean() + + +if __name__ == '__main__': + # To ensure any files referenced via a relative path will begin from the diectory in which this file resides + os.chdir(os.path.dirname(os.path.realpath(__file__))) + + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET, + help='When used, environment will be deployed and provisioned') + parser.add_argument('-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET, + help='When used, the environment will be removed') + parser.add_argument('-i', '--clean-image', dest='clean_image', nargs='?', default=ARG_NOT_SET, + help='When cleaning, if this is set, the image will be cleaned too') + parser.add_argument('-e', '--env', dest='environment', required=True, + help='The environment configuration YAML file - REQUIRED') + parser.add_argument('-l', '--log-level', dest='log_level', default='INFO', help='Logging Level (INFO|DEBUG)') + args = parser.parse_args() + + if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET: + print 'Must enter either -d for deploy or -c for cleaning up and environment' + exit(1) + if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET: + print 'Cannot enter both options -d/--deploy and -c/--clean' + exit(1) + main(args) diff --git a/snaps/file_utils.py b/snaps/file_utils.py new file mode 100644 index 0000000..f66ac17 --- /dev/null +++ b/snaps/file_utils.py @@ -0,0 +1,108 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import urllib2 +import logging + +import yaml + +__author__ = 'spisarski' + +""" +Utilities for basic file handling +""" + +logger = logging.getLogger('file_utils') + + +def file_exists(file_path): + """ + Returns True if the image file already exists and throws an exception if the path is a directory + :return: + """ + if os.path.exists(file_path): + if os.path.isdir(file_path): + return False + return os.path.isfile(file_path) + return False + + +def get_file(file_path): + """ + Returns True if the image file has already been downloaded + :return: the image file object + :raise Exception when file cannot be found + """ + if file_exists(file_path): + return open(file_path, 'r') + else: + raise Exception('File with path cannot be found - ' + file_path) + + +def download(url, dest_path): + """ + Download a file to a destination path given a URL + :rtype : File object + """ + name = url.rsplit('/')[-1] + dest = dest_path + '/' + name + try: + # Override proxy settings to use localhost to download file + proxy_handler = urllib2.ProxyHandler({}) + opener = urllib2.build_opener(proxy_handler) + urllib2.install_opener(opener) + response = urllib2.urlopen(url) + except (urllib2.HTTPError, urllib2.URLError): + raise Exception + + with open(dest, 'wb') as f: + f.write(response.read()) + return f + + +def read_yaml(config_file_path): + """ + Reads the yaml file and returns a dictionary object representation + :param config_file_path: The file path to config + :return: a dictionary + """ + logger.debug('Attempting to load configuration file - ' + config_file_path) + with open(config_file_path) as config_file: + config = yaml.safe_load(config_file) + logger.info('Loaded configuration') + config_file.close() + logger.info('Closing configuration file') + return config + + +def read_os_env_file(os_env_filename): + """ + Reads the OS environment source file and returns a map of each key/value + Will ignore lines beginning with a '#' and will replace any single or double quotes contained within the value + :param os_env_filename: The name of the OS environment file to read + :return: a dictionary + """ + if os_env_filename: + logger.info('Attempting to read OS environment file - ' + os_env_filename) + out = {} + for line in open(os_env_filename): + line = line.lstrip() + if not line.startswith('#') and line.startswith('export '): + line = line.lstrip('export ').strip() + tokens = line.split('=') + if len(tokens) > 1: + # Remove leading and trailing ' & " characters from value + out[tokens[0]] = tokens[1].lstrip('\'').lstrip('\"').rstrip('\'').rstrip('\"') + return out diff --git a/snaps/openstack/__init__.py b/snaps/openstack/__init__.py new file mode 100644 index 0000000..e3e876e --- /dev/null +++ b/snaps/openstack/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' diff --git a/snaps/openstack/create_flavor.py b/snaps/openstack/create_flavor.py new file mode 100644 index 0000000..68a7080 --- /dev/null +++ b/snaps/openstack/create_flavor.py @@ -0,0 +1,179 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from novaclient.exceptions import NotFound + +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_image') + +DEFAULT_METADATA = {'hw:mem_page_size': 'any'} + + +class OpenStackFlavor: + """ + Class responsible for creating a user in OpenStack + """ + + def __init__(self, os_creds, flavor_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param flavor_settings: The flavor settings + :return: + """ + self.__os_creds = os_creds + self.flavor_settings = flavor_settings + self.__flavor = None + self.__nova = nova_utils.nova_client(self.__os_creds) + + def create(self, cleanup=False): + """ + Creates the image in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack flavor object + """ + self.__flavor = nova_utils.get_flavor_by_name(self.__nova, self.flavor_settings.name) + if self.__flavor: + logger.info('Found flavor with name - ' + self.flavor_settings.name) + elif not cleanup: + self.__flavor = nova_utils.create_flavor(self.__nova, self.flavor_settings) + if self.flavor_settings.metadata: + self.__flavor.set_keys(self.flavor_settings.metadata) + self.__flavor = nova_utils.get_flavor_by_name(self.__nova, self.flavor_settings.name) + else: + logger.info('Did not create flavor due to cleanup mode') + + return self.__flavor + + def clean(self): + """ + Cleanse environment of all artifacts + :return: void + """ + if self.__flavor: + try: + nova_utils.delete_flavor(self.__nova, self.__flavor) + except NotFound: + pass + + self.__flavor = None + + def get_flavor(self): + """ + Returns the OpenStack flavor object + :return: + """ + return self.__flavor + + +class FlavorSettings: + """ + Configuration settings for OpenStack flavor creation + """ + + def __init__(self, config=None, name=None, flavor_id='auto', ram=None, disk=None, vcpus=None, ephemeral=0, swap=0, + rxtx_factor=1.0, is_public=True, metadata=DEFAULT_METADATA): + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the flavor's name (required) + :param flavor_id: the string ID (default 'auto') + :param ram: the required RAM in MB (required) + :param disk: the size of the root disk in GB (required) + :param vcpus: the number of virtual CPUs (required) + :param ephemeral: the size of the ephemeral disk in GB (default 0) + :param swap: the size of the dedicated swap disk in GB (default 0) + :param rxtx_factor: the receive/transmit factor to be set on ports if backend supports + QoS extension (default 1.0) + :param is_public: denotes whether or not the flavor is public (default True) + :param metadata: freeform dict() for special metadata (default hw:mem_page_size=any) + """ + + if config: + self.name = config.get('name') + + if config.get('flavor_id'): + self.flavor_id = config['flavor_id'] + else: + self.flavor_id = flavor_id + + self.ram = config.get('ram') + self.disk = config.get('disk') + self.vcpus = config.get('vcpus') + + if config.get('ephemeral'): + self.ephemeral = config['ephemeral'] + else: + self.ephemeral = ephemeral + + if config.get('swap'): + self.swap = config['swap'] + else: + self.swap = swap + + if config.get('rxtx_factor'): + self.rxtx_factor = config['rxtx_factor'] + else: + self.rxtx_factor = rxtx_factor + + if config.get('is_public') is not None: + self.is_public = config['is_public'] + else: + self.is_public = is_public + + if config.get('metadata'): + self.metadata = config['metadata'] + else: + self.metadata = metadata + else: + self.name = name + self.flavor_id = flavor_id + self.ram = ram + self.disk = disk + self.vcpus = vcpus + self.ephemeral = ephemeral + self.swap = swap + self.rxtx_factor = rxtx_factor + self.is_public = is_public + self.metadata = metadata + + if not self.name or not self.ram or not self.disk or not self.vcpus: + raise Exception('The attributes name, ram, disk, and vcpus are required for FlavorSettings') + + if not isinstance(self.ram, int): + raise Exception('The ram attribute must be a integer') + + if not isinstance(self.disk, int): + raise Exception('The ram attribute must be a integer') + + if not isinstance(self.vcpus, int): + raise Exception('The vcpus attribute must be a integer') + + if self.ephemeral and not isinstance(self.ephemeral, int): + raise Exception('The ephemeral attribute must be an integer') + + if self.swap and not isinstance(self.swap, int): + raise Exception('The swap attribute must be an integer') + + if self.rxtx_factor and not isinstance(self.rxtx_factor, (int, float)): + raise Exception('The is_public attribute must be an integer or float') + + if self.is_public and not isinstance(self.is_public, bool): + raise Exception('The is_public attribute must be a boolean') diff --git a/snaps/openstack/create_image.py b/snaps/openstack/create_image.py new file mode 100644 index 0000000..e1b8d94 --- /dev/null +++ b/snaps/openstack/create_image.py @@ -0,0 +1,188 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import time + +from glanceclient.exc import HTTPNotFound + +from snaps.openstack.utils import glance_utils, nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_image') + +IMAGE_ACTIVE_TIMEOUT = 600 +POLL_INTERVAL = 3 +STATUS_ACTIVE = 'active' + + +class OpenStackImage: + """ + Class responsible for creating an image in OpenStack + """ + + def __init__(self, os_creds, image_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param image_settings: The image settings + :return: + """ + self.__os_creds = os_creds + self.image_settings = image_settings + self.__image = None + self.__glance = glance_utils.glance_client(os_creds) + + def create(self, cleanup=False): + """ + Creates the image in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack Image object + """ + from snaps.openstack.utils import nova_utils + nova = nova_utils.nova_client(self.__os_creds) + + self.__image = glance_utils.get_image(nova, self.__glance, self.image_settings.name) + if self.__image: + logger.info('Found image with name - ' + self.image_settings.name) + return self.__image + elif not cleanup: + self.__image = glance_utils.create_image(self.__glance, self.image_settings) + logger.info('Creating image') + if self.image_active(block=True): + logger.info('Image is now active with name - ' + self.image_settings.name) + return self.__image + else: + raise Exception('Image did not activate in the alloted amount of time') + else: + logger.info('Did not create image due to cleanup mode') + + return self.__image + + def clean(self): + """ + Cleanse environment of all artifacts + :return: void + """ + if self.__image: + try: + glance_utils.delete_image(self.__glance, self.__image) + except HTTPNotFound: + pass + self.__image = None + + def get_image(self): + """ + Returns the OpenStack image object as it was populated when create() was called + :return: the object + """ + return self.__image + + def image_active(self, block=False, timeout=IMAGE_ACTIVE_TIMEOUT, poll_interval=POLL_INTERVAL): + """ + Returns true when the image status returns the value of expected_status_code + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param timeout: The timeout value + :param poll_interval: The polling interval in seconds + :return: T/F + """ + return self._image_status_check(STATUS_ACTIVE, block, timeout, poll_interval) + + def _image_status_check(self, expected_status_code, block, timeout, poll_interval): + """ + Returns true when the image status returns the value of expected_status_code + :param expected_status_code: instance status evaluated with this string value + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param timeout: The timeout value + :param poll_interval: The polling interval in seconds + :return: T/F + """ + # sleep and wait for image status change + if block: + start = time.time() + else: + start = time.time() - timeout + + while timeout > time.time() - start: + status = self._status(expected_status_code) + if status: + logger.info('Image is active with name - ' + self.image_settings.name) + return True + + logger.debug('Retry querying image status in ' + str(poll_interval) + ' seconds') + time.sleep(poll_interval) + logger.debug('Image status query timeout in ' + str(timeout - (time.time() - start))) + + logger.error('Timeout checking for image status for ' + expected_status_code) + return False + + def _status(self, expected_status_code): + """ + Returns True when active else False + :param expected_status_code: instance status evaluated with this string value + :return: T/F + """ + # TODO - Place this API call into glance_utils. + nova = nova_utils.nova_client(self.__os_creds) + instance = glance_utils.get_image(nova, self.__glance, self.image_settings.name) + # instance = self.__glance.images.get(self.__image) + if not instance: + logger.warn('Cannot find instance with id - ' + self.__image.id) + return False + + if instance.status == 'ERROR': + raise Exception('Instance had an error during deployment') + logger.debug('Instance status is - ' + instance.status) + return instance.status == expected_status_code + + +class ImageSettings: + def __init__(self, config=None, name=None, image_user=None, img_format=None, url=None, image_file=None, + nic_config_pb_loc=None): + """ + + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the image's name (required) + :param image_user: the image's default sudo user (required) + :param img_format: the image type (required) + :param url: the image download location (requires url or img_file) + :param image_file: the image file location (requires url or img_file) + :param nic_config_pb_loc: the file location to the Ansible Playbook that can configure multiple NICs + """ + + if config: + self.name = config.get('name') + self.image_user = config.get('image_user') + self.format = config.get('format') + self.url = config.get('download_url') + self.image_file = config.get('image_file') + self.nic_config_pb_loc = config.get('nic_config_pb_loc') + else: + self.name = name + self.image_user = image_user + self.format = img_format + self.url = url + self.image_file = image_file + self.nic_config_pb_loc = nic_config_pb_loc + + if not self.name or not self.image_user or not self.format: + raise Exception("The attributes name, image_user, format, and url are required for ImageSettings") + + if not self.url and not self.image_file: + raise Exception('URL or image file must be set') + + if self.url and self.image_file: + raise Exception('Please set either URL or image file, not both') diff --git a/snaps/openstack/create_instance.py b/snaps/openstack/create_instance.py new file mode 100644 index 0000000..caddc05 --- /dev/null +++ b/snaps/openstack/create_instance.py @@ -0,0 +1,739 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import time + +from neutronclient.common.exceptions import PortNotFoundClient +from novaclient.exceptions import NotFound + +from snaps.openstack.utils import glance_utils +from snaps.openstack.utils import neutron_utils +from snaps.openstack.create_network import PortSettings +from snaps.provisioning import ansible_utils +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_instance') + +POLL_INTERVAL = 3 +STATUS_ACTIVE = 'ACTIVE' +STATUS_DELETED = 'DELETED' + + +class OpenStackVmInstance: + """ + Class responsible for creating a VM instance in OpenStack + """ + + def __init__(self, os_creds, instance_settings, image_settings, keypair_settings=None): + """ + Constructor + :param os_creds: The connection credentials to the OpenStack API + :param instance_settings: Contains the settings for this VM + :param image_settings: The OpenStack image object settings + :param keypair_settings: The keypair metadata (Optional) + :raises Exception + """ + self.__os_creds = os_creds + + self.__nova = nova_utils.nova_client(self.__os_creds) + self.__neutron = neutron_utils.neutron_client(self.__os_creds) + + self.instance_settings = instance_settings + self.image_settings = image_settings + self.keypair_settings = keypair_settings + + # TODO - get rid of FIP list and only use the dict(). Need to fix populating this object when already exists + self.__floating_ips = list() + self.__floating_ip_dict = dict() + + # Instantiated in self.create() + self.__ports = list() + + # Note: this object does not change after the VM becomes active + self.__vm = None + + def create(self, cleanup=False, block=False): + """ + Creates a VM instance + :param cleanup: When true, only perform lookups for OpenStack objects. + :param block: Thread will block until instance has either become active, error, or timeout waiting. + Additionally, when True, floating IPs will not be applied until VM is active. + :return: The VM reference object + """ + try: + self.__ports = self.__setup_ports(self.instance_settings.port_settings, cleanup) + self.__lookup_existing_vm_by_name() + if not self.__vm and not cleanup: + self.__create_vm(block) + return self.__vm + except Exception as e: + logger.exception('Error occurred while setting up instance') + self.clean() + raise e + + def __lookup_existing_vm_by_name(self): + """ + Populates the member variables 'self.vm' and 'self.floating_ips' if a VM with the same name already exists + within the project + """ + servers = nova_utils.get_servers_by_name(self.__nova, self.instance_settings.name) + for server in servers: + if server.name == self.instance_settings.name: + self.__vm = server + logger.info('Found existing machine with name - ' + self.instance_settings.name) + fips = self.__nova.floating_ips.list() + for fip in fips: + if fip.instance_id == server.id: + self.__floating_ips.append(fip) + # TODO - Determine a means to associate to the FIP configuration and add to FIP map + + def __create_vm(self, block=False): + """ + Responsible for creating the VM instance + :param block: Thread will block until instance has either become active, error, or timeout waiting. + Floating IPs will be assigned after active when block=True + """ + nics = [] + for key, port in self.__ports: + kv = dict() + kv['port-id'] = port['port']['id'] + nics.append(kv) + + logger.info('Creating VM with name - ' + self.instance_settings.name) + keypair_name = None + if self.keypair_settings: + keypair_name = self.keypair_settings.name + + flavor = nova_utils.get_flavor_by_name(self.__nova, self.instance_settings.flavor) + if not flavor: + raise Exception('Flavor not found with name - ' + self.instance_settings.flavor) + + image = glance_utils.get_image(self.__nova, glance_utils.glance_client(self.__os_creds), + self.image_settings.name) + if image: + self.__vm = self.__nova.servers.create( + name=self.instance_settings.name, + flavor=flavor, + image=image, + nics=nics, + key_name=keypair_name, + security_groups=list(self.instance_settings.security_group_names), + userdata=self.instance_settings.userdata, + availability_zone=self.instance_settings.availability_zone) + else: + raise Exception('Cannot create instance, image cannot be located with name ' + self.image_settings.name) + + logger.info('Created instance with name - ' + self.instance_settings.name) + + if block: + self.vm_active(block=True) + + self.__apply_floating_ips() + + def __apply_floating_ips(self): + """ + Applies the configured floating IPs to the necessary ports + """ + port_dict = dict() + for key, port in self.__ports: + port_dict[key] = port + + # Apply floating IPs + for floating_ip_setting in self.instance_settings.floating_ip_settings: + port = port_dict.get(floating_ip_setting.port_name) + + if not port: + raise Exception('Cannot find port object with name - ' + floating_ip_setting.port_name) + + # Setup Floating IP only if there is a router with an external gateway + ext_gateway = self.__ext_gateway_by_router(floating_ip_setting.router_name) + if ext_gateway: + subnet = neutron_utils.get_subnet_by_name(self.__neutron, floating_ip_setting.subnet_name) + floating_ip = nova_utils.create_floating_ip(self.__nova, ext_gateway) + self.__floating_ips.append(floating_ip) + self.__floating_ip_dict[floating_ip_setting.name] = floating_ip + + logger.info('Created floating IP ' + floating_ip.ip + ' via router - ' + + floating_ip_setting.router_name) + self.__add_floating_ip(floating_ip, port, subnet) + else: + raise Exception('Unable to add floating IP to port,' + + ' cannot locate router with an external gateway ') + + def __ext_gateway_by_router(self, router_name): + """ + Returns network name for the external network attached to a router or None if not found + :param router_name: The name of the router to lookup + :return: the external network name or None + """ + router = neutron_utils.get_router_by_name(self.__neutron, router_name) + if router and router['router'].get('external_gateway_info'): + network = neutron_utils.get_network_by_id(self.__neutron, + router['router']['external_gateway_info']['network_id']) + if network: + return network['network']['name'] + return None + + def clean(self): + """ + Destroys the VM instance + """ + + # Cleanup floating IPs + for floating_ip in self.__floating_ips: + try: + logger.info('Deleting Floating IP - ' + floating_ip.ip) + nova_utils.delete_floating_ip(self.__nova, floating_ip) + except Exception as e: + logger.error('Error deleting Floating IP - ' + e.message) + self.__floating_ips = list() + self.__floating_ip_dict = dict() + + # Cleanup ports + for name, port in self.__ports: + logger.info('Deleting Port - ' + name) + try: + neutron_utils.delete_port(self.__neutron, port) + except PortNotFoundClient as e: + logger.warn('Unexpected error deleting port - ' + e.message) + pass + self.__ports = list() + + # Cleanup VM + if self.__vm: + try: + logger.info('Deleting VM instance - ' + self.instance_settings.name) + nova_utils.delete_vm_instance(self.__nova, self.__vm) + except Exception as e: + logger.error('Error deleting VM - ' + str(e)) + + # Block until instance cannot be found or returns the status of DELETED + logger.info('Checking deletion status') + + try: + if self.vm_deleted(block=True): + logger.info('VM has been properly deleted VM with name - ' + self.instance_settings.name) + self.__vm = None + else: + logger.error('VM not deleted within the timeout period of ' + + str(self.instance_settings.vm_delete_timeout) + ' seconds') + except Exception as e: + logger.error('Unexpected error while checking VM instance status - ' + e.message) + + def __setup_ports(self, port_settings, cleanup): + """ + Returns the previously configured ports or creates them if they do not exist + :param port_settings: A list of PortSetting objects + :param cleanup: When true, only perform lookups for OpenStack objects. + :return: a list of OpenStack port tuples where the first member is the port name and the second is the port + object + """ + ports = list() + + for port_setting in port_settings: + # First check to see if network already has this port + # TODO/FIXME - this could potentially cause problems if another port with the same name exists + # VM has the same network/port name pair + found = False + + # TODO/FIXME - should we not be iterating on ports for the specific network in question as unique port names + # seem to only be important by network + existing_ports = self.__neutron.list_ports()['ports'] + for existing_port in existing_ports: + if existing_port['name'] == port_setting.name: + ports.append((port_setting.name, {'port': existing_port})) + found = True + break + + if not found and not cleanup: + ports.append((port_setting.name, neutron_utils.create_port(self.__neutron, self.__os_creds, + port_setting))) + + return ports + + def __add_floating_ip(self, floating_ip, port, subnet, timeout=30, poll_interval=POLL_INTERVAL): + """ + Returns True when active else False + TODO - Make timeout and poll_interval configurable... + """ + ip = None + + if subnet: + # Take IP of subnet if there is one configured on which to place the floating IP + for fixed_ip in port['port']['fixed_ips']: + if fixed_ip['subnet_id'] == subnet['subnet']['id']: + ip = fixed_ip['ip_address'] + break + else: + # Simply take the first + ip = port['port']['fixed_ips'][0]['ip_address'] + + if ip: + count = timeout / poll_interval + while count > 0: + logger.debug('Attempting to add floating IP to instance') + try: + self.__vm.add_floating_ip(floating_ip, ip) + logger.info('Added floating IP ' + floating_ip.ip + ' to port IP - ' + ip + + ' on instance - ' + self.instance_settings.name) + return + except Exception as e: + logger.debug('Retry adding floating IP to instance. Last attempt failed with - ' + e.message) + time.sleep(poll_interval) + count -= 1 + pass + else: + raise Exception('Unable find IP address on which to place the floating IP') + + logger.error('Timeout attempting to add the floating IP to instance.') + raise Exception('Timeout while attempting add floating IP to instance') + + def get_os_creds(self): + """ + Returns the OpenStack credentials used to create these objects + :return: the credentials + """ + return self.__os_creds + + def get_vm_inst(self): + """ + Returns the latest version of this server object from OpenStack + :return: Server object + """ + return nova_utils.get_latest_server_object(self.__nova, self.__vm) + + def get_port_ip(self, port_name, subnet_name=None): + """ + Returns the first IP for the port corresponding with the port_name parameter when subnet_name is None + else returns the IP address that corresponds to the subnet_name parameter + :param port_name: the name of the port from which to return the IP + :param subnet_name: the name of the subnet attached to this IP + :return: the IP or None if not found + """ + port = self.get_port_by_name(port_name) + if port: + port_dict = port['port'] + if subnet_name: + subnet = neutron_utils.get_subnet_by_name(self.__neutron, subnet_name) + if not subnet: + logger.warn('Cannot retrieve port IP as subnet could not be located with name - ' + subnet_name) + return None + for fixed_ip in port_dict['fixed_ips']: + if fixed_ip['subnet_id'] == subnet['subnet']['id']: + return fixed_ip['ip_address'] + else: + fixed_ips = port_dict['fixed_ips'] + if fixed_ips and len(fixed_ips) > 0: + return fixed_ips[0]['ip_address'] + return None + + def get_port_mac(self, port_name): + """ + Returns the first IP for the port corresponding with the port_name parameter + TODO - Add in the subnet as an additional parameter as a port may have multiple fixed_ips + :param port_name: the name of the port from which to return the IP + :return: the IP or None if not found + """ + port = self.get_port_by_name(port_name) + if port: + port_dict = port['port'] + return port_dict['mac_address'] + return None + + def get_port_by_name(self, port_name): + """ + Retrieves the OpenStack port object by its given name + :param port_name: the name of the port + :return: the OpenStack port object or None if not exists + """ + for key, port in self.__ports: + if key == port_name: + return port + logger.warn('Cannot find port with name - ' + port_name) + return None + + def config_nics(self): + """ + Responsible for configuring NICs on RPM systems where the instance has more than one configured port + :return: None + """ + if len(self.__ports) > 1 and len(self.__floating_ips) > 0: + if self.vm_active(block=True) and self.vm_ssh_active(block=True): + for key, port in self.__ports: + port_index = self.__ports.index((key, port)) + if port_index > 0: + nic_name = 'eth' + repr(port_index) + self.__config_nic(nic_name, port, self.__get_first_provisioning_floating_ip().ip) + logger.info('Configured NIC - ' + nic_name + ' on VM - ' + self.instance_settings.name) + + def __get_first_provisioning_floating_ip(self): + """ + Returns the first floating IP tagged with the Floating IP name if exists else the first one found + :return: + """ + for floating_ip_setting in self.instance_settings.floating_ip_settings: + if floating_ip_setting.provisioning: + fip = self.__floating_ip_dict.get(floating_ip_setting.name) + if fip: + return fip + elif len(self.__floating_ips) > 0: + return self.__floating_ips[0] + + def __config_nic(self, nic_name, port, floating_ip): + """ + Although ports/NICs can contain multiple IPs, this code currently only supports the first. + + Your CWD at this point must be the <repo dir>/python directory. + TODO - fix this restriction. + + :param nic_name: Name of the interface + :param port: The port information containing the expected IP values. + :param floating_ip: The floating IP on which to apply the playbook. + """ + ip = port['port']['fixed_ips'][0]['ip_address'] + variables = { + 'floating_ip': floating_ip, + 'nic_name': nic_name, + 'nic_ip': ip + } + + if self.image_settings.nic_config_pb_loc and self.keypair_settings: + ansible_utils.apply_playbook(self.image_settings.nic_config_pb_loc, + [floating_ip], self.get_image_user(), self.keypair_settings.private_filepath, + variables, self.__os_creds.proxy_settings) + else: + logger.warn('VM ' + self.instance_settings.name + ' cannot self configure NICs eth1++. ' + + 'No playbook or keypairs found.') + + def get_image_user(self): + """ + Returns the instance sudo_user if it has been configured in the instance_settings else it returns the + image_settings.image_user value + """ + if self.instance_settings.sudo_user: + return self.instance_settings.sudo_user + else: + return self.image_settings.image_user + + def vm_deleted(self, block=False, poll_interval=POLL_INTERVAL): + """ + Returns true when the VM status returns the value of expected_status_code or instance retrieval throws + a NotFound exception. + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param poll_interval: The polling interval in seconds + :return: T/F + """ + try: + return self.__vm_status_check(STATUS_DELETED, block, self.instance_settings.vm_delete_timeout, + poll_interval) + except NotFound as e: + logger.debug("Instance not found when querying status for " + STATUS_DELETED + ' with message ' + e.message) + return True + + def vm_active(self, block=False, poll_interval=POLL_INTERVAL): + """ + Returns true when the VM status returns the value of expected_status_code + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param poll_interval: The polling interval in seconds + :return: T/F + """ + return self.__vm_status_check(STATUS_ACTIVE, block, self.instance_settings.vm_boot_timeout, poll_interval) + + def __vm_status_check(self, expected_status_code, block, timeout, poll_interval): + """ + Returns true when the VM status returns the value of expected_status_code + :param expected_status_code: instance status evaluated with this string value + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param timeout: The timeout value + :param poll_interval: The polling interval in seconds + :return: T/F + """ + # sleep and wait for VM status change + if block: + start = time.time() + else: + start = time.time() - timeout + + while timeout > time.time() - start: + status = self.__status(expected_status_code) + if status: + logger.info('VM is - ' + expected_status_code) + return True + + logger.debug('Retry querying VM status in ' + str(poll_interval) + ' seconds') + time.sleep(poll_interval) + logger.debug('VM status query timeout in ' + str(timeout - (time.time() - start))) + + logger.error('Timeout checking for VM status for ' + expected_status_code) + return False + + def __status(self, expected_status_code): + """ + Returns True when active else False + :param expected_status_code: instance status evaluated with this string value + :return: T/F + """ + instance = self.__nova.servers.get(self.__vm.id) + if not instance: + logger.warn('Cannot find instance with id - ' + self.__vm.id) + return False + + if instance.status == 'ERROR': + raise Exception('Instance had an error during deployment') + logger.debug('Instance status [' + self.instance_settings.name + '] is - ' + instance.status) + return instance.status == expected_status_code + + def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL): + """ + Returns true when the VM can be accessed via SSH + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param poll_interval: The polling interval + :return: T/F + """ + # sleep and wait for VM status change + logger.info('Checking if VM is active') + + timeout = self.instance_settings.ssh_connect_timeout + + if self.vm_active(block=True): + if block: + start = time.time() + else: + start = time.time() - timeout + + while timeout > time.time() - start: + status = self.__ssh_active() + if status: + logger.info('SSH is active for VM instance') + return True + + logger.debug('Retry SSH connection in ' + str(poll_interval) + ' seconds') + time.sleep(poll_interval) + logger.debug('SSH connection timeout in ' + str(timeout - (time.time() - start))) + + logger.error('Timeout attempting to connect with VM via SSH') + return False + + def __ssh_active(self): + """ + Returns True when can create a SSH session else False + :return: T/F + """ + if len(self.__floating_ips) > 0: + ssh = self.ssh_client() + if ssh: + return True + return False + + def get_floating_ip(self, fip_name=None): + """ + Returns the floating IP object byt name if found, else the first known, else None + :param fip_name: the name of the floating IP to return + :return: the SSH client or None + """ + fip = None + if fip_name and self.__floating_ip_dict.get(fip_name): + return self.__floating_ip_dict.get(fip_name) + if not fip and len(self.__floating_ips) > 0: + return self.__floating_ips[0] + return None + + def ssh_client(self, fip_name=None): + """ + Returns an SSH client using the name or the first known floating IP if exists, else None + :param fip_name: the name of the floating IP to return + :return: the SSH client or None + """ + fip = self.get_floating_ip(fip_name) + if fip: + return ansible_utils.ssh_client(self.__floating_ips[0].ip, self.get_image_user(), + self.keypair_settings.private_filepath, + proxy_settings=self.__os_creds.proxy_settings) + else: + logger.warn('Cannot return an SSH client. No Floating IP configured') + + def add_security_group(self, security_group): + """ + Adds a security group to this VM. Call will block until VM is active. + :param security_group: the OpenStack security group object + :return True if successful else False + """ + self.vm_active(block=True) + + if not security_group: + logger.warn('Security group object is None, cannot add') + return False + + try: + nova_utils.add_security_group(self.__nova, self.get_vm_inst(), security_group['security_group']['name']) + return True + except NotFound as e: + logger.warn('Security group not added - ' + e.message) + return False + + def remove_security_group(self, security_group): + """ + Removes a security group to this VM. Call will block until VM is active. + :param security_group: the OpenStack security group object + :return True if successful else False + """ + self.vm_active(block=True) + + if not security_group: + logger.warn('Security group object is None, cannot add') + return False + + try: + nova_utils.remove_security_group(self.__nova, self.get_vm_inst(), security_group['security_group']['name']) + return True + except NotFound as e: + logger.warn('Security group not added - ' + e.message) + return False + + +class VmInstanceSettings: + """ + Class responsible for holding configuration setting for a VM Instance + """ + def __init__(self, config=None, name=None, flavor=None, port_settings=list(), security_group_names=set(), + floating_ip_settings=list(), sudo_user=None, vm_boot_timeout=900, + vm_delete_timeout=300, ssh_connect_timeout=180, availability_zone=None, userdata=None): + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the name of the VM + :param flavor: the VM's flavor + :param port_settings: the port configuration settings + :param security_group_names: a set of names of the security groups to add to the VM + :param floating_ip_settings: the floating IP configuration settings + :param sudo_user: the sudo user of the VM that will override the instance_settings.image_user when trying to + connect to the VM + :param vm_boot_timeout: the amount of time a thread will sleep waiting for an instance to boot + :param vm_delete_timeout: the amount of time a thread will sleep waiting for an instance to be deleted + :param ssh_connect_timeout: the amount of time a thread will sleep waiting obtaining an SSH connection to a VM + :param availability_zone: the name of the compute server on which to deploy the VM (optional) + :param userdata: the cloud-init script to run after the VM has been started + """ + if config: + self.name = config.get('name') + self.flavor = config.get('flavor') + self.sudo_user = config.get('sudo_user') + self.userdata = config.get('userdata') + + self.port_settings = list() + if config.get('ports'): + for port_config in config['ports']: + if isinstance(port_config, PortSettings): + self.port_settings.append(port_config) + else: + self.port_settings.append(PortSettings(config=port_config['port'])) + + if config.get('security_group_names'): + if isinstance(config['security_group_names'], list): + self.security_group_names = set(config['security_group_names']) + elif isinstance(config['security_group_names'], set): + self.security_group_names = config['security_group_names'] + elif isinstance(config['security_group_names'], basestring): + self.security_group_names = [config['security_group_names']] + else: + raise Exception('Invalid data type for security_group_names attribute') + else: + self.security_group_names = set() + + self.floating_ip_settings = list() + if config.get('floating_ips'): + for floating_ip_config in config['floating_ips']: + if isinstance(floating_ip_config, FloatingIpSettings): + self.floating_ip_settings.append(floating_ip_config) + else: + self.floating_ip_settings.append(FloatingIpSettings(config=floating_ip_config['floating_ip'])) + + if config.get('vm_boot_timeout'): + self.vm_boot_timeout = config['vm_boot_timeout'] + else: + self.vm_boot_timeout = vm_boot_timeout + + if config.get('vm_delete_timeout'): + self.vm_delete_timeout = config['vm_delete_timeout'] + else: + self.vm_delete_timeout = vm_delete_timeout + + if config.get('ssh_connect_timeout'): + self.ssh_connect_timeout = config['ssh_connect_timeout'] + else: + self.ssh_connect_timeout = ssh_connect_timeout + + if config.get('availability_zone'): + self.availability_zone = config['availability_zone'] + else: + self.availability_zone = None + else: + self.name = name + self.flavor = flavor + self.port_settings = port_settings + self.security_group_names = security_group_names + self.floating_ip_settings = floating_ip_settings + self.sudo_user = sudo_user + self.vm_boot_timeout = vm_boot_timeout + self.vm_delete_timeout = vm_delete_timeout + self.ssh_connect_timeout = ssh_connect_timeout + self.availability_zone = availability_zone + self.userdata = userdata + + if not self.name or not self.flavor: + raise Exception('Instance configuration requires the attributes: name, flavor') + + +class FloatingIpSettings: + """ + Class responsible for holding configuration settings for a floating IP + """ + def __init__(self, config=None, name=None, port_name=None, router_name=None, subnet_name=None, provisioning=True): + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the name of the floating IP + :param port_name: the name of the router to the external network + :param router_name: the name of the router to the external network + :param subnet_name: the name of the subnet on which to attach the floating IP + :param provisioning: when true, this floating IP can be used for provisioning + + TODO - provisioning flag is a hack as I have only observed a single Floating IPs that actually works on + an instance. Multiple floating IPs placed on different subnets from the same port are especially troublesome + as you cannot predict which one will actually connect. For now, it is recommended not to setup multiple + floating IPs on an instance unless absolutely necessary. + """ + if config: + self.name = config.get('name') + self.port_name = config.get('port_name') + self.router_name = config.get('router_name') + self.subnet_name = config.get('subnet_name') + if config.get('provisioning') is not None: + self.provisioning = config['provisioning'] + else: + self.provisioning = provisioning + else: + self.name = name + self.port_name = port_name + self.router_name = router_name + self.subnet_name = subnet_name + self.provisioning = provisioning + + if not self.name or not self.port_name or not self.router_name: + raise Exception('The attributes name, port_name and router_name are required for FloatingIPSettings') diff --git a/snaps/openstack/create_keypairs.py b/snaps/openstack/create_keypairs.py new file mode 100644 index 0000000..ea7c811 --- /dev/null +++ b/snaps/openstack/create_keypairs.py @@ -0,0 +1,121 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os + +from Crypto.PublicKey import RSA +from novaclient.exceptions import NotFound + +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackKeypair') + + +class OpenStackKeypair: + """ + Class responsible for creating a keypair in OpenStack + """ + + def __init__(self, os_creds, keypair_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param keypair_settings: The settings used to create a keypair + """ + self.__os_creds = os_creds + self.keypair_settings = keypair_settings + self.__nova = nova_utils.nova_client(os_creds) + + # Attributes instantiated on create() + self.__keypair = None + + def create(self, cleanup=False): + """ + Responsible for creating the keypair object. + :param cleanup: Denotes whether or not this is being called for cleanup or not + """ + logger.info('Creating keypair %s...' % self.keypair_settings.name) + + try: + self.__keypair = nova_utils.get_keypair_by_name(self.__nova, self.keypair_settings.name) + + if not self.__keypair and not cleanup: + if self.keypair_settings.public_filepath and os.path.isfile(self.keypair_settings.public_filepath): + logger.info("Uploading existing keypair") + self.__keypair = nova_utils.upload_keypair_file(self.__nova, self.keypair_settings.name, + self.keypair_settings.public_filepath) + else: + logger.info("Creating new keypair") + # TODO - Make this value configurable + keys = RSA.generate(1024) + self.__keypair = nova_utils.upload_keypair(self.__nova, self.keypair_settings.name, + keys.publickey().exportKey('OpenSSH')) + nova_utils.save_keys_to_files(keys, self.keypair_settings.public_filepath, + self.keypair_settings.private_filepath) + + return self.__keypair + except Exception as e: + logger.error('Unexpected error creating keypair named - ' + self.keypair_settings.name) + self.clean() + raise Exception(e.message) + + def clean(self): + """ + Removes and deletes the keypair. + """ + if self.__keypair: + try: + nova_utils.delete_keypair(self.__nova, self.__keypair) + except NotFound: + pass + self.__keypair = None + + def get_keypair(self): + """ + Returns the OpenStack keypair object + :return: + """ + return self.__keypair + + +class KeypairSettings: + """ + Class representing a keypair configuration + """ + + def __init__(self, config=None, name=None, public_filepath=None, private_filepath=None): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The keypair name. + :param public_filepath: The path to/from the filesystem where the public key file is or will be stored + :param private_filepath: The path where the generated private key file will be stored + :return: + """ + + if config: + self.name = config.get('name') + self.public_filepath = config.get('public_filepath') + self.private_filepath = config.get('private_filepath') + else: + self.name = name + self.public_filepath = public_filepath + self.private_filepath = private_filepath + + if not self.name: + raise Exception('The attributes name, public_filepath, and private_filepath are required') diff --git a/snaps/openstack/create_network.py b/snaps/openstack/create_network.py new file mode 100644 index 0000000..a214ba1 --- /dev/null +++ b/snaps/openstack/create_network.py @@ -0,0 +1,519 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from neutronclient.common.exceptions import NotFound + +from snaps.openstack.utils import keystone_utils, neutron_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackNetwork') + + +class OpenStackNetwork: + """ + Class responsible for creating a network in OpenStack + """ + + def __init__(self, os_creds, network_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param network_settings: The settings used to create a network + """ + self.__os_creds = os_creds + self.network_settings = network_settings + self.__neutron = neutron_utils.neutron_client(self.__os_creds) + + # Attributes instantiated on create() + self.__network = None + self.__subnets = list() + + def create(self, cleanup=False): + """ + Responsible for creating not only the network but then a private subnet, router, and an interface to the router. + :param cleanup: When true, only perform lookups for OpenStack objects. + :return: the created network object or None + """ + try: + logger.info('Creating neutron network %s...' % self.network_settings.name) + net_inst = neutron_utils.get_network(self.__neutron, self.network_settings.name, + self.network_settings.get_project_id(self.__os_creds)) + if net_inst: + self.__network = net_inst + else: + if not cleanup: + self.__network = neutron_utils.create_network(self.__neutron, self.__os_creds, + self.network_settings) + else: + logger.info('Network does not exist and will not create as in cleanup mode') + return + logger.debug("Network '%s' created successfully" % self.__network['network']['id']) + + logger.debug('Creating Subnets....') + for subnet_setting in self.network_settings.subnet_settings: + sub_inst = neutron_utils.get_subnet_by_name(self.__neutron, subnet_setting.name) + if sub_inst: + self.__subnets.append(sub_inst) + logger.debug("Subnet '%s' created successfully" % sub_inst['subnet']['id']) + else: + if not cleanup: + self.__subnets.append(neutron_utils.create_subnet(self.__neutron, subnet_setting, + self.__os_creds, self.__network)) + + return self.__network + except Exception as e: + logger.error('Unexpected exception thrown while creating network - ' + str(e)) + self.clean() + raise e + + def clean(self): + """ + Removes and deletes all items created in reverse order. + """ + for subnet in self.__subnets: + try: + logger.info('Deleting subnet with name ' + subnet['subnet']['name']) + neutron_utils.delete_subnet(self.__neutron, subnet) + except NotFound as e: + logger.warn('Error deleting subnet with message - ' + e.message) + pass + self.__subnets = list() + + if self.__network: + try: + neutron_utils.delete_network(self.__neutron, self.__network) + except NotFound: + pass + + self.__network = None + + def get_network(self): + """ + Returns the created OpenStack network object + :return: the OpenStack network object + """ + return self.__network + + def get_subnets(self): + """ + Returns the OpenStack subnet objects + :return: + """ + return self.__subnets + + +class NetworkSettings: + """ + Class representing a network configuration + """ + + def __init__(self, config=None, name=None, admin_state_up=True, shared=None, project_name=None, + external=False, network_type=None, physical_network=None, subnet_settings=list()): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The network name. + :param admin_state_up: The administrative status of the network. True = up / False = down (default True) + :param shared: Boolean value indicating whether this network is shared across all projects/tenants. By default, + only administrative users can change this value. + :param project_name: Admin-only. The name of the project that will own the network. This project can be + different from the project that makes the create network request. However, only + administrative users can specify a project ID other than their own. You cannot change this + value through authorization policies. + :param external: when true, will setup an external network (default False). + :param network_type: the type of network (i.e. vlan|flat). + :param physical_network: the name of the physical network (this is required when network_type is 'flat') + :param subnet_settings: List of SubnetSettings objects. + :return: + """ + + self.project_id = None + + if config: + self.name = config.get('name') + if config.get('admin_state_up') is not None: + self.admin_state_up = bool(config['admin_state_up']) + else: + self.admin_state_up = admin_state_up + + if config.get('shared') is not None: + self.shared = bool(config['shared']) + else: + self.shared = None + + self.project_name = config.get('project_name') + + if config.get('external') is not None: + self.external = bool(config.get('external')) + else: + self.external = external + + self.network_type = config.get('network_type') + self.physical_network = config.get('physical_network') + + self.subnet_settings = list() + if config.get('subnets'): + for subnet_config in config['subnets']: + self.subnet_settings.append(SubnetSettings(config=subnet_config['subnet'])) + + else: + self.name = name + self.admin_state_up = admin_state_up + self.shared = shared + self.project_name = project_name + self.external = external + self.network_type = network_type + self.physical_network = physical_network + self.subnet_settings = subnet_settings + + if not self.name or len(self.name) < 1: + raise Exception('Name required for networks') + + def get_project_id(self, os_creds): + """ + Returns the project ID for a given project_name or None + :param os_creds: the credentials required for keystone client retrieval + :return: the ID or None + """ + if self.project_id: + return self.project_id + else: + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + if project: + return project.id + + return None + + def dict_for_neutron(self, os_creds): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + + :param os_creds: the OpenStack credentials + :return: the dictionary object + """ + out = dict() + + if self.name: + out['name'] = self.name + if self.admin_state_up is not None: + out['admin_state_up'] = self.admin_state_up + if self.shared: + out['shared'] = self.shared + if self.project_name: + project_id = self.get_project_id(os_creds) + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.network_type: + out['provider:network_type'] = self.network_type + if self.physical_network: + out['provider:physical_network'] = self.physical_network + if self.external: + out['router:external'] = self.external + return {'network': out} + + +class SubnetSettings: + """ + Class representing a subnet configuration + """ + + def __init__(self, config=None, cidr=None, ip_version=4, name=None, project_name=None, start=None, + end=None, gateway_ip=None, enable_dhcp=None, dns_nameservers=None, host_routes=None, destination=None, + nexthop=None, ipv6_ra_mode=None, ipv6_address_mode=None): + """ + Constructor - all parameters are optional except cidr (subnet mask) + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param cidr: The CIDR. REQUIRED if config parameter is None + :param ip_version: The IP version, which is 4 or 6. + :param name: The subnet name. + :param project_name: The name of the project who owns the network. Only administrative users can specify a + project ID other than their own. You cannot change this value through authorization + policies. + :param start: The start address for the allocation pools. + :param end: The end address for the allocation pools. + :param gateway_ip: The gateway IP address. + :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is disabled. + :param dns_nameservers: A list of DNS name servers for the subnet. Specify each name server as an IP address + and separate multiple entries with a space. For example [8.8.8.7 8.8.8.8]. + :param host_routes: A list of host route dictionaries for the subnet. For example: + "host_routes":[ + { + "destination":"0.0.0.0/0", + "nexthop":"123.456.78.9" + }, + { + "destination":"192.168.0.0/24", + "nexthop":"192.168.0.1" + } + ] + :param destination: The destination for static route + :param nexthop: The next hop for the destination. + :param ipv6_ra_mode: A valid value is dhcpv6-stateful, dhcpv6-stateless, or slaac. + :param ipv6_address_mode: A valid value is dhcpv6-stateful, dhcpv6-stateless, or slaac. + :raise: Exception when config does not have or cidr values are None + """ + if not dns_nameservers: + dns_nameservers = ['8.8.8.8'] + + if config: + self.cidr = config['cidr'] + if config.get('ip_version'): + self.ip_version = config['ip_version'] + else: + self.ip_version = ip_version + + # Optional attributes that can be set after instantiation + self.name = config.get('name') + self.project_name = config.get('project_name') + self.start = config.get('start') + self.end = config.get('end') + self.gateway_ip = config.get('gateway_ip') + self.enable_dhcp = config.get('enable_dhcp') + + if config.get('dns_nameservers'): + self.dns_nameservers = config.get('dns_nameservers') + else: + self.dns_nameservers = dns_nameservers + + self.host_routes = config.get('host_routes') + self.destination = config.get('destination') + self.nexthop = config.get('nexthop') + self.ipv6_ra_mode = config.get('ipv6_ra_mode') + self.ipv6_address_mode = config.get('ipv6_address_mode') + else: + # Required attributes + self.cidr = cidr + self.ip_version = ip_version + + # Optional attributes that can be set after instantiation + self.name = name + self.project_name = project_name + self.start = start + self.end = end + self.gateway_ip = gateway_ip + self.enable_dhcp = enable_dhcp + self.dns_nameservers = dns_nameservers + self.host_routes = host_routes + self.destination = destination + self.nexthop = nexthop + self.ipv6_ra_mode = ipv6_ra_mode + self.ipv6_address_mode = ipv6_address_mode + + if not self.name or not self.cidr: + raise Exception('Name and cidr required for subnets') + + def dict_for_neutron(self, os_creds, network=None): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + :param os_creds: the OpenStack credentials + :param network: (Optional) the network object on which the subnet will be created + :return: the dictionary object + """ + out = { + 'cidr': self.cidr, + 'ip_version': self.ip_version, + } + + if network: + out['network_id'] = network['network']['id'] + if self.name: + out['name'] = self.name + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + project_id = None + if project: + project_id = project.id + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.start and self.end: + out['allocation_pools'] = [{'start': self.start, 'end': self.end}] + if self.gateway_ip: + out['gateway_ip'] = self.gateway_ip + if self.enable_dhcp is not None: + out['enable_dhcp'] = self.enable_dhcp + if self.dns_nameservers and len(self.dns_nameservers) > 0: + out['dns_nameservers'] = self.dns_nameservers + if self.host_routes and len(self.host_routes) > 0: + out['host_routes'] = self.host_routes + if self.destination: + out['destination'] = self.destination + if self.nexthop: + out['nexthop'] = self.nexthop + if self.ipv6_ra_mode: + out['ipv6_ra_mode'] = self.ipv6_ra_mode + if self.ipv6_address_mode: + out['ipv6_address_mode'] = self.ipv6_address_mode + return out + + +class PortSettings: + """ + Class representing a port configuration + """ + + def __init__(self, config=None, name=None, network_name=None, admin_state_up=True, project_name=None, + mac_address=None, ip_addrs=None, fixed_ips=None, security_groups=None, allowed_address_pairs=None, + opt_value=None, opt_name=None, device_owner=None, device_id=None): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: A symbolic name for the port. + :param network_name: The name of the network on which to create the port. + :param admin_state_up: A boolean value denoting the administrative status of the port. True = up / False = down + :param project_name: The name of the project who owns the network. Only administrative users can specify a + project ID other than their own. You cannot change this value through authorization + policies. + :param mac_address: The MAC address. If you specify an address that is not valid, a Bad Request (400) status + code is returned. If you do not specify a MAC address, OpenStack Networking tries to + allocate one. If a failure occurs, a Service Unavailable (503) status code is returned. + :param ip_addrs: A list of dict objects where each contains two keys 'subnet_name' and 'ip' values which will + get mapped to self.fixed_ips. + These values will be directly translated into the fixed_ips dict + :param fixed_ips: A dict where the key is the subnet IDs and value is the IP address to assign to the port + :param security_groups: One or more security group IDs. + :param allowed_address_pairs: A dictionary containing a set of zero or more allowed address pairs. An address + pair contains an IP address and MAC address. + :param opt_value: The extra DHCP option value. + :param opt_name: The extra DHCP option name. + :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. + :param device_id: The ID of the device that uses this port. For example, a virtual server. + :return: + """ + self.network = None + + if config: + self.name = config.get('name') + self.network_name = config.get('network_name') + + if config.get('admin_state_up') is not None: + self.admin_state_up = bool(config['admin_state_up']) + else: + self.admin_state_up = admin_state_up + + self.project_name = config.get('project_name') + self.mac_address = config.get('mac_address') + self.ip_addrs = config.get('ip_addrs') + self.fixed_ips = config.get('fixed_ips') + self.security_groups = config.get('security_groups') + self.allowed_address_pairs = config.get('allowed_address_pairs') + self.opt_value = config.get('opt_value') + self.opt_name = config.get('opt_name') + self.device_owner = config.get('device_owner') + self.device_id = config.get('device_id') + else: + self.name = name + self.network_name = network_name + self.admin_state_up = admin_state_up + self.project_name = project_name + self.mac_address = mac_address + self.ip_addrs = ip_addrs + self.fixed_ips = fixed_ips + self.security_groups = security_groups + self.allowed_address_pairs = allowed_address_pairs + self.opt_value = opt_value + self.opt_name = opt_name + self.device_owner = device_owner + self.device_id = device_id + + if not self.name or not self.network_name: + raise Exception('The attributes neutron, name, and network_name are required for PortSettings') + + def __set_fixed_ips(self, neutron): + """ + Sets the self.fixed_ips value + :param neutron: the Neutron client + :return: None + """ + if not self.fixed_ips and self.ip_addrs: + self.fixed_ips = list() + + for ip_addr_dict in self.ip_addrs: + subnet = neutron_utils.get_subnet_by_name(neutron, ip_addr_dict['subnet_name']) + if subnet: + self.fixed_ips.append({'ip_address': ip_addr_dict['ip'], 'subnet_id': subnet['subnet']['id']}) + else: + raise Exception('Invalid port configuration, subnet does not exist with name - ' + + ip_addr_dict['subnet_name']) + + def dict_for_neutron(self, neutron, os_creds): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + :param neutron: the Neutron client + :param os_creds: the OpenStack credentials + :return: the dictionary object + """ + self.__set_fixed_ips(neutron) + + out = dict() + + project_id = None + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + if project: + project_id = project.id + + if not self.network: + self.network = neutron_utils.get_network(neutron, self.network_name, project_id) + if not self.network: + raise Exception('Cannot locate network with name - ' + self.network_name) + + out['network_id'] = self.network['network']['id'] + + if self.admin_state_up is not None: + out['admin_state_up'] = self.admin_state_up + if self.name: + out['name'] = self.name + if self.project_name: + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.mac_address: + out['mac_address'] = self.mac_address + if self.fixed_ips and len(self.fixed_ips) > 0: + out['fixed_ips'] = self.fixed_ips + if self.security_groups: + out['security_groups'] = self.security_groups + if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0: + out['allowed_address_pairs'] = self.allowed_address_pairs + if self.opt_value: + out['opt_value'] = self.opt_value + if self.opt_name: + out['opt_name'] = self.opt_name + if self.device_owner: + out['device_owner'] = self.device_owner + if self.device_id: + out['device_id'] = self.device_id + return {'port': out} diff --git a/snaps/openstack/create_project.py b/snaps/openstack/create_project.py new file mode 100644 index 0000000..60f9ed0 --- /dev/null +++ b/snaps/openstack/create_project.py @@ -0,0 +1,139 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from keystoneclient.exceptions import NotFound + +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_image') + + +class OpenStackProject: + """ + Class responsible for creating a project/project in OpenStack + """ + + def __init__(self, os_creds, project_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param project_settings: The project's settings + :return: + """ + self.__os_creds = os_creds + self.project_settings = project_settings + self.__project = None + self.__role = None + self.__keystone = keystone_utils.keystone_client(self.__os_creds) + + def create(self, cleanup=False): + """ + Creates the image in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack Image object + """ + try: + self.__project = keystone_utils.get_project(keystone=self.__keystone, + project_name=self.project_settings.name) + if self.__project: + logger.info('Found project with name - ' + self.project_settings.name) + elif not cleanup: + self.__project = keystone_utils.create_project(self.__keystone, self.project_settings) + else: + logger.info('Did not create image due to cleanup mode') + except Exception as e: + logger.error('Unexpected error. Rolling back') + self.clean() + raise Exception(e.message) + + return self.__project + + def clean(self): + """ + Cleanse environment of all artifacts + :return: void + """ + if self.__project: + try: + keystone_utils.delete_project(self.__keystone, self.__project) + except NotFound: + pass + self.__project = None + + if self.__role: + try: + keystone_utils.delete_role(self.__keystone, self.__role) + except NotFound: + pass + self.__project = None + + def get_project(self): + """ + Returns the OpenStack project object populated on create() + :return: + """ + return self.__project + + def assoc_user(self, user): + """ + The user object to associate with the project + :param user: the OpenStack user object to associate with project + :return: + """ + if not self.__role: + self.__role = keystone_utils.create_role(self.__keystone, self.project_settings.name + '-role') + + keystone_utils.assoc_user_to_project(self.__keystone, self.__role, user, self.__project) + + +class ProjectSettings: + """ + Class to hold the configuration settings required for creating OpenStack project objects + """ + def __init__(self, config=None, name=None, domain='default', description=None, enabled=True): + + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the project's name (required) + :param domain: the project's domain name (default 'default'). Field is used for v3 clients + :param description: the description (optional) + :param enabled: denotes whether or not the user is enabled (default True) + """ + + if config: + self.name = config.get('name') + if config.get('domain'): + self.domain = config['domain'] + else: + self.domain = domain + + self.description = config.get('description') + if config.get('enabled') is not None: + self.enabled = config['enabled'] + else: + self.enabled = enabled + else: + self.name = name + self.domain = domain + self.description = description + self.enabled = enabled + + if not self.name: + raise Exception("The attribute name is required for ProjectSettings") diff --git a/snaps/openstack/create_router.py b/snaps/openstack/create_router.py new file mode 100644 index 0000000..70c6b76 --- /dev/null +++ b/snaps/openstack/create_router.py @@ -0,0 +1,244 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from neutronclient.common.exceptions import NotFound + +from snaps.openstack.create_network import PortSettings +from snaps.openstack.utils import neutron_utils, keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackNetwork') + + +class OpenStackRouter: + """ + Class responsible for creating a router in OpenStack + """ + + def __init__(self, os_creds, router_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param router_settings: The settings used to create a router object (must be an instance of the + RouterSettings class) + """ + self.__os_creds = os_creds + + if not router_settings: + raise Exception('router_settings is required') + + self.router_settings = router_settings + self.__neutron = neutron_utils.neutron_client(os_creds) + + # Attributes instantiated on create() + self.__router = None + self.__internal_subnets = list() + self.__internal_router_interface = None + + # Dict where the port object is the key and any newly created router interfaces are the value + self.__ports = list() + + def create(self, cleanup=False): + """ + Responsible for creating the router. + :param cleanup: When true, only perform lookups for OpenStack objects. + :return: the router object + """ + logger.debug('Creating Router with name - ' + self.router_settings.name) + try: + existing = False + router_inst = neutron_utils.get_router_by_name(self.__neutron, self.router_settings.name) + if router_inst: + self.__router = router_inst + existing = True + else: + if not cleanup: + self.__router = neutron_utils.create_router(self.__neutron, self.__os_creds, self.router_settings) + + for internal_subnet_name in self.router_settings.internal_subnets: + internal_subnet = neutron_utils.get_subnet_by_name(self.__neutron, internal_subnet_name) + if internal_subnet: + self.__internal_subnets.append(internal_subnet) + if internal_subnet and not cleanup and not existing: + logger.debug('Adding router to subnet...') + self.__internal_router_interface = neutron_utils.add_interface_router( + self.__neutron, self.__router, subnet=internal_subnet) + else: + raise Exception('Subnet not found with name ' + internal_subnet_name) + + for port_setting in self.router_settings.port_settings: + port = neutron_utils.get_port_by_name(self.__neutron, port_setting.name) + logger.info('Retrieved port ' + port_setting.name + ' for router - ' + self.router_settings.name) + if port: + self.__ports.append(port) + + if not port and not cleanup and not existing: + port = neutron_utils.create_port(self.__neutron, self.__os_creds, port_setting) + if port: + logger.info('Created port ' + port_setting.name + ' for router - ' + self.router_settings.name) + self.__ports.append(port) + neutron_utils.add_interface_router(self.__neutron, self.__router, port=port) + else: + raise Exception('Error creating port with name - ' + port_setting.name) + + return self.__router + except Exception as e: + self.clean() + raise Exception(e.message) + + def clean(self): + """ + Removes and deletes all items created in reverse order. + """ + for port in self.__ports: + logger.info('Removing router interface from router ' + self.router_settings.name + + ' and port ' + port['port']['name']) + try: + neutron_utils.remove_interface_router(self.__neutron, self.__router, port=port) + except NotFound: + pass + self.__ports = list() + + for internal_subnet in self.__internal_subnets: + logger.info('Removing router interface from router ' + self.router_settings.name + + ' and subnet ' + internal_subnet['subnet']['name']) + try: + neutron_utils.remove_interface_router(self.__neutron, self.__router, subnet=internal_subnet) + except NotFound: + pass + self.__internal_subnets = list() + + if self.__router: + logger.info('Removing router ' + self.router_settings.name) + try: + neutron_utils.delete_router(self.__neutron, self.__router) + except NotFound: + pass + self.__router = None + + def get_router(self): + """ + Returns the OpenStack router object + :return: + """ + return self.__router + + def get_internal_router_interface(self): + """ + Returns the OpenStack internal router interface object + :return: + """ + return self.__internal_router_interface + + +class RouterSettings: + """ + Class representing a router configuration + """ + + def __init__(self, config=None, name=None, project_name=None, external_gateway=None, + admin_state_up=True, external_fixed_ips=None, internal_subnets=list(), + port_settings=list()): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The router name. + :param project_name: The name of the project who owns the network. Only administrative users can specify a + project ID other than their own. You cannot change this value through authorization + policies. + :param external_gateway: Name of the external network to which to route + :param admin_state_up: The administrative status of the router. True = up / False = down (default True) + :param enable_snat: Boolean value. Enable Source NAT (SNAT) attribute. Default is True. To persist this + attribute value, set the enable_snat_by_default option in the neutron.conf file. + :param external_fixed_ips: Dictionary containing the IP address parameters. + :param internal_subnets: List of subnet names to which to connect this router for Floating IP purposes + :param port_settings: List of PortSettings objects + :return: + """ + if config: + self.name = config.get('name') + self.project_name = config.get('project_name') + self.external_gateway = config.get('external_gateway') + + self.admin_state_up = config.get('admin_state_up') + self.enable_snat = config.get('enable_snat') + self.external_fixed_ips = config.get('external_fixed_ips') + if config.get('internal_subnets'): + self.internal_subnets = config['internal_subnets'] + else: + self.internal_subnets = internal_subnets + + self.port_settings = list() + if config.get('interfaces'): + interfaces = config['interfaces'] + for interface in interfaces: + if interface.get('port'): + self.port_settings.append(PortSettings(config=interface['port'])) + else: + self.name = name + self.project_name = project_name + self.external_gateway = external_gateway + self.admin_state_up = admin_state_up + self.external_fixed_ips = external_fixed_ips + self.internal_subnets = internal_subnets + self.port_settings = port_settings + + if not self.name: + raise Exception('Name is required') + + def dict_for_neutron(self, neutron, os_creds): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + :param neutron: The neutron client to retrieve external network information if necessary + :param os_creds: The OpenStack credentials + :return: the dictionary object + """ + out = dict() + ext_gw = dict() + + project_id = None + + if self.name: + out['name'] = self.name + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + project_id = None + if project: + project_id = project.id + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.admin_state_up is not None: + out['admin_state_up'] = self.admin_state_up + if self.external_gateway: + ext_net = neutron_utils.get_network(neutron, self.external_gateway, project_id) + if ext_net: + ext_gw['network_id'] = ext_net['network']['id'] + out['external_gateway_info'] = ext_gw + else: + raise Exception('Could not find the external network named - ' + self.external_gateway) + + #TODO: Enable SNAT option for Router + #TODO: Add external_fixed_ips Tests + + return {'router': out} diff --git a/snaps/openstack/create_security_group.py b/snaps/openstack/create_security_group.py new file mode 100644 index 0000000..fc1ee98 --- /dev/null +++ b/snaps/openstack/create_security_group.py @@ -0,0 +1,521 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import enum +from neutronclient.common.exceptions import NotFound +from snaps.openstack.utils import neutron_utils +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackSecurityGroup') + + +class OpenStackSecurityGroup: + """ + Class responsible for creating Security Groups + """ + + def __init__(self, os_creds, sec_grp_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param sec_grp_settings: The settings used to create a security group + """ + self.__os_creds = os_creds + self.sec_grp_settings = sec_grp_settings + self.__neutron = neutron_utils.neutron_client(os_creds) + self.__keystone = keystone_utils.keystone_client(os_creds) + + # Attributes instantiated on create() + self.__security_group = None + + # dict where the rule settings object is the key + self.__rules = dict() + + def create(self, cleanup=False): + """ + Responsible for creating the security group. + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: the OpenStack security group object + """ + logger.info('Creating security group %s...' % self.sec_grp_settings.name) + + self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name) + if not self.__security_group and not cleanup: + # Create the security group + self.__security_group = neutron_utils.create_security_group(self.__neutron, self.__keystone, + self.sec_grp_settings) + + # Get the rules added for free + auto_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group) + + ctr = 0 + for auto_rule in auto_rules: + auto_rule_setting = self.__generate_rule_setting(auto_rule) + self.__rules[auto_rule_setting] = auto_rule + ctr += 1 + + # Create the custom rules + for sec_grp_rule_setting in self.sec_grp_settings.rule_settings: + custom_rule = neutron_utils.create_security_group_rule(self.__neutron, sec_grp_rule_setting) + self.__rules[sec_grp_rule_setting] = custom_rule + + # Refresh security group object to reflect the new rules added to it + self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name) + else: + # Populate rules + existing_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group) + + for existing_rule in existing_rules: + # For Custom Rules + rule_setting = self.__get_setting_from_rule(existing_rule) + ctr = 0 + if not rule_setting: + # For Free Rules + rule_setting = self.__generate_rule_setting(existing_rule) + ctr += 1 + + self.__rules[rule_setting] = existing_rule + + return self.__security_group + + def __generate_rule_setting(self, rule): + """ + Creates a SecurityGroupRuleSettings object for a given rule + :param rule: the rule from which to create the SecurityGroupRuleSettings object + :return: the newly instantiated SecurityGroupRuleSettings object + """ + rule_dict = rule['security_group_rule'] + sec_grp_name = None + if rule_dict['security_group_id']: + sec_grp = neutron_utils.get_security_group_by_id(self.__neutron, rule_dict['security_group_id']) + if sec_grp: + sec_grp_name = sec_grp['security_group']['name'] + + setting = SecurityGroupRuleSettings(description=rule_dict['description'], + direction=rule_dict['direction'], ethertype=rule_dict['ethertype'], + port_range_min=rule_dict['port_range_min'], + port_range_max=rule_dict['port_range_max'], protocol=rule_dict['protocol'], + remote_group_id=rule_dict['remote_group_id'], + remote_ip_prefix=rule_dict['remote_ip_prefix'], sec_grp_name=sec_grp_name) + return setting + + def clean(self): + """ + Removes and deletes the rules then the security group. + """ + for setting, rule in self.__rules.iteritems(): + try: + neutron_utils.delete_security_group_rule(self.__neutron, rule) + except NotFound as e: + logger.warn('Rule not found, cannot delete - ' + e.message) + pass + self.__rules = dict() + + if self.__security_group: + try: + neutron_utils.delete_security_group(self.__neutron, self.__security_group) + except NotFound as e: + logger.warn('Security Group not found, cannot delete - ' + e.message) + + self.__security_group = None + + def get_security_group(self): + """ + Returns the OpenStack security group object + :return: + """ + return self.__security_group + + def get_rules(self): + """ + Returns the associated rules + :return: + """ + return self.__rules + + def add_rule(self, rule_setting): + """ + Adds a rule to this security group + :param rule_setting: the rule configuration + """ + rule_setting.sec_grp_name = self.sec_grp_settings.name + new_rule = neutron_utils.create_security_group_rule(self.__neutron, rule_setting) + self.__rules[rule_setting] = new_rule + self.sec_grp_settings.rule_settings.append(rule_setting) + + def remove_rule(self, rule_id=None, rule_setting=None): + """ + Removes a rule to this security group by id, name, or rule_setting object + :param rule_id: the rule's id + :param rule_setting: the rule's setting object + """ + rule_to_remove = None + if rule_id or rule_setting: + if rule_id: + rule_to_remove = neutron_utils.get_rule_by_id(self.__neutron, self.__security_group, rule_id) + elif rule_setting: + rule_to_remove = self.__rules.get(rule_setting) + + if rule_to_remove: + neutron_utils.delete_security_group_rule(self.__neutron, rule_to_remove) + rule_setting = self.__get_setting_from_rule(rule_to_remove) + if rule_setting: + self.__rules.pop(rule_setting) + else: + logger.warn('Rule setting is None, cannot remove rule') + + def __get_setting_from_rule(self, rule): + """ + Returns the associated RuleSetting object for a given rule + :param rule: the Rule object + :return: the associated RuleSetting object or None + """ + for rule_setting in self.sec_grp_settings.rule_settings: + if rule_setting.rule_eq(rule): + return rule_setting + return None + + +class SecurityGroupSettings: + """ + Class representing a keypair configuration + """ + + def __init__(self, config=None, name=None, description=None, project_name=None, + rule_settings=list()): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The keypair name. + :param description: The security group's description + :param project_name: The name of the project under which the security group will be created + :return: + """ + if config: + self.name = config.get('name') + self.description = config.get('description') + self.project_name = config.get('project_name') + self.rule_settings = list() + if config.get('rules') and type(config['rules']) is list: + for config_rule in config['rules']: + self.rule_settings.append(SecurityGroupRuleSettings(config=config_rule)) + else: + self.name = name + self.description = description + self.project_name = project_name + self.rule_settings = rule_settings + + if not self.name: + raise Exception('The attribute name is required') + + for rule_setting in self.rule_settings: + if rule_setting.sec_grp_name is not self.name: + raise Exception('Rule settings must correspond with the name of this security group') + + def dict_for_neutron(self, keystone): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + :param keystone: the Keystone client + :return: the dictionary object + """ + out = dict() + + if self.name: + out['name'] = self.name + if self.description: + out['description'] = self.description + if self.project_name: + project = keystone_utils.get_project(keystone, self.project_name) + project_id = None + if project: + project_id = project.id + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + + return {'security_group': out} + + +class Direction(enum.Enum): + """ + A rule's direction + """ + ingress = 'ingress' + egress = 'egress' + + +class Protocol(enum.Enum): + """ + A rule's protocol + """ + icmp = 'icmp' + tcp = 'tcp' + udp = 'udp' + null = 'null' + + +class Ethertype(enum.Enum): + """ + A rule's ethertype + """ + IPv4 = 4 + IPv6 = 6 + + +class SecurityGroupRuleSettings: + """ + Class representing a keypair configuration + """ + + def __init__(self, config=None, sec_grp_name=None, description=None, direction=None, + remote_group_id=None, protocol=None, ethertype=None, port_range_min=None, port_range_max=None, + sec_grp_rule=None, remote_ip_prefix=None): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param sec_grp_name: The security group's name on which to add the rule. (required) + :param description: The rule's description + :param direction: An enumeration of type create_security_group.RULE_DIRECTION (required) + :param remote_group_id: The group ID to associate with this rule (this should be changed to group name + once snaps support Groups) (optional) + :param protocol: An enumeration of type create_security_group.RULE_PROTOCOL or a string value that will be + mapped accordingly (optional) + :param ethertype: An enumeration of type create_security_group.RULE_ETHERTYPE (optional) + :param port_range_min: The minimum port number in the range that is matched by the security group rule. When + the protocol is TCP or UDP, this value must be <= port_range_max. When the protocol is + ICMP, this value must be an ICMP type. + :param port_range_max: The maximum port number in the range that is matched by the security group rule. When + the protocol is TCP or UDP, this value must be <= port_range_max. When the protocol is + ICMP, this value must be an ICMP type. + :param sec_grp_rule: The OpenStack rule object to a security group rule object to associate + (note: Cannot be set using the config object nor can I see any real uses for this + parameter) + :param remote_ip_prefix: The remote IP prefix to associate with this metering rule packet (optional) + + TODO - Need to support the tenant... + """ + + if config: + self.description = config.get('description') + self.sec_grp_name = config.get('sec_grp_name') + self.remote_group_id = config.get('remote_group_id') + self.direction = None + if config.get('direction'): + self.direction = map_direction(config['direction']) + + self.protocol = None + if config.get('protocol'): + self.protocol = map_protocol(config['protocol']) + else: + self.protocol = Protocol.null + + self.ethertype = None + if config.get('ethertype'): + self.ethertype = map_ethertype(config['ethertype']) + + self.port_range_min = config.get('port_range_min') + self.port_range_max = config.get('port_range_max') + self.remote_ip_prefix = config.get('remote_ip_prefix') + else: + self.description = description + self.sec_grp_name = sec_grp_name + self.remote_group_id = remote_group_id + self.direction = map_direction(direction) + self.protocol = map_protocol(protocol) + self.ethertype = map_ethertype(ethertype) + self.port_range_min = port_range_min + self.port_range_max = port_range_max + self.sec_grp_rule = sec_grp_rule + self.remote_ip_prefix = remote_ip_prefix + + if not self.direction or not self.sec_grp_name: + raise Exception('direction and sec_grp_name are required') + + def dict_for_neutron(self, neutron): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + :param neutron: the neutron client for performing lookups + :return: the dictionary object + """ + out = dict() + + if self.description: + out['description'] = self.description + if self.direction: + out['direction'] = self.direction.name + if self.port_range_min: + out['port_range_min'] = self.port_range_min + if self.port_range_max: + out['port_range_max'] = self.port_range_max + if self.ethertype: + out['ethertype'] = self.ethertype.name + if self.protocol: + out['protocol'] = self.protocol.name + if self.sec_grp_name: + sec_grp = neutron_utils.get_security_group(neutron, self.sec_grp_name) + if sec_grp: + out['security_group_id'] = sec_grp['security_group']['id'] + else: + raise Exception('Cannot locate security group with name - ' + self.sec_grp_name) + if self.remote_group_id: + out['remote_group_id'] = self.remote_group_id + if self.sec_grp_rule: + out['security_group_rule'] = self.sec_grp_rule + if self.remote_ip_prefix: + out['remote_ip_prefix'] = self.remote_ip_prefix + + return {'security_group_rule': out} + + def rule_eq(self, rule): + """ + Returns True if this setting created the rule + :param rule: the rule to evaluate + :return: T/F + """ + rule_dict = rule['security_group_rule'] + + if self.description is not None: + if rule_dict['description'] is not None and rule_dict['description'] != '': + return False + elif self.description != rule_dict['description']: + if rule_dict['description'] != '': + return False + + if self.direction.name != rule_dict['direction']: + return False + + if self.ethertype and rule_dict.get('ethertype'): + if self.ethertype.name != rule_dict['ethertype']: + return False + + if self.port_range_min and rule_dict.get('port_range_min'): + if self.port_range_min != rule_dict['port_range_min']: + return False + + if self.port_range_max and rule_dict.get('port_range_max'): + if self.port_range_max != rule_dict['port_range_max']: + return False + + if self.protocol and rule_dict.get('protocol'): + if self.protocol.name != rule_dict['protocol']: + return False + + if self.remote_group_id and rule_dict.get('remote_group_id'): + if self.remote_group_id != rule_dict['remote_group_id']: + return False + + if self.remote_ip_prefix and rule_dict.get('remote_ip_prefix'): + if self.remote_ip_prefix != rule_dict['remote_ip_prefix']: + return False + + return True + + def __eq__(self, other): + return self.description == other.description and \ + self.direction == other.direction and \ + self.port_range_min == other.port_range_min and \ + self.port_range_max == other.port_range_max and \ + self.ethertype == other.ethertype and \ + self.protocol == other.protocol and \ + self.sec_grp_name == other.sec_grp_name and \ + self.remote_group_id == other.remote_group_id and \ + self.sec_grp_rule == other.sec_grp_rule and \ + self.remote_ip_prefix == other.remote_ip_prefix + + def __hash__(self): + return hash((self.sec_grp_name, self.description, self.direction, self.remote_group_id, + self.protocol, self.ethertype, self.port_range_min, self.port_range_max, self.sec_grp_rule, + self.remote_ip_prefix)) + + +def map_direction(direction): + """ + Takes a the direction value maps it to the Direction enum. When None return None + :param direction: the direction value + :return: the Direction enum object + :raise: Exception if value is invalid + """ + if not direction: + return None + if type(direction) is Direction: + return direction + elif isinstance(direction, basestring): + if direction == 'egress': + return Direction.egress + elif direction == 'ingress': + return Direction.ingress + else: + raise Exception('Invalid Direction - ' + direction) + else: + raise Exception('Invalid Direction object - ' + str(direction)) + + +def map_protocol(protocol): + """ + Takes a the protocol value maps it to the Protocol enum. When None return None + :param protocol: the protocol value + :return: the Protocol enum object + :raise: Exception if value is invalid + """ + if not protocol: + return None + elif type(protocol) is Protocol: + return protocol + elif isinstance(protocol, basestring): + if protocol == 'icmp': + return Protocol.icmp + elif protocol == 'tcp': + return Protocol.tcp + elif protocol == 'udp': + return Protocol.udp + elif protocol == 'null': + return Protocol.null + else: + raise Exception('Invalid Protocol - ' + protocol) + else: + raise Exception('Invalid Protocol object - ' + str(protocol)) + + +def map_ethertype(ethertype): + """ + Takes a the ethertype value maps it to the Ethertype enum. When None return None + :param ethertype: the ethertype value + :return: the Ethertype enum object + :raise: Exception if value is invalid + """ + if not ethertype: + return None + elif type(ethertype) is Ethertype: + return ethertype + elif isinstance(ethertype, basestring): + if ethertype == 'IPv6': + return Ethertype.IPv6 + elif ethertype == 'IPv4': + return Ethertype.IPv4 + else: + raise Exception('Invalid Ethertype - ' + ethertype) + else: + raise Exception('Invalid Ethertype object - ' + str(ethertype)) diff --git a/snaps/openstack/create_user.py b/snaps/openstack/create_user.py new file mode 100644 index 0000000..a8d0fcc --- /dev/null +++ b/snaps/openstack/create_user.py @@ -0,0 +1,137 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from keystoneclient.exceptions import NotFound +from snaps.openstack.os_credentials import OSCreds + +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_user') + + +class OpenStackUser: + """ + Class responsible for creating a user in OpenStack + """ + + def __init__(self, os_creds, user_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param user_settings: The user settings + :return: + """ + self.__os_creds = os_creds + self.user_settings = user_settings + self.__user = None + self.__keystone = keystone_utils.keystone_client(self.__os_creds) + + def create(self, cleanup=False): + """ + Creates the user in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack user object + """ + self.__user = keystone_utils.get_user(self.__keystone, self.user_settings.name) + if self.__user: + logger.info('Found user with name - ' + self.user_settings.name) + elif not cleanup: + self.__user = keystone_utils.create_user(self.__keystone, self.user_settings) + else: + logger.info('Did not create user due to cleanup mode') + + return self.__user + + def clean(self): + """ + Cleanse environment of user + :return: void + """ + if self.__user: + try: + keystone_utils.delete_user(self.__keystone, self.__user) + except NotFound: + pass + self.__user = None + + def get_user(self): + """ + Returns the OpenStack user object populated in create() + :return: the Object or None if not created + """ + return self.__user + + def get_os_creds(self, project_name=None): + """ + Returns an OSCreds object based on this user account and a project + :param project_name: the name of the project to leverage in the credentials + :return: + """ + return OSCreds(username=self.user_settings.name, + password=self.user_settings.password, + auth_url=self.__os_creds.auth_url, + project_name=project_name, + identity_api_version=self.__os_creds.identity_api_version, + user_domain_id=self.__os_creds.user_domain_id, + project_domain_id=self.__os_creds.project_domain_id, + proxy_settings=self.__os_creds.proxy_settings) + + +class UserSettings: + def __init__(self, config=None, name=None, password=None, project_name=None, domain_name='default', email=None, + enabled=True): + + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the user's name (required) + :param password: the user's password (required) + :param project_name: the user's primary project name (optional) + :param domain_name: the user's domain name (default='default'). For v3 APIs + :param email: the user's email address (optional) + :param enabled: denotes whether or not the user is enabled (default True) + """ + + if config: + self.name = config.get('name') + self.password = config.get('password') + self.project_name = config.get('project_name') + self.email = config.get('email') + + if config.get('domain_name'): + self.domain_name = config['domain_name'] + else: + self.domain_name = domain_name + + if config.get('enabled') is not None: + self.enabled = config['enabled'] + else: + self.enabled = enabled + else: + self.name = name + self.password = password + self.project_name = project_name + self.email = email + self.enabled = enabled + self.domain_name = domain_name + + if not self.name or not self.password: + raise Exception('The attributes name and password are required for UserSettings') + + if not isinstance(self.enabled, bool): + raise Exception('The attribute enabled must be of type boolean') diff --git a/snaps/openstack/os_credentials.py b/snaps/openstack/os_credentials.py new file mode 100644 index 0000000..c173bf7 --- /dev/null +++ b/snaps/openstack/os_credentials.py @@ -0,0 +1,103 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' + + +class OSCreds: + """ + Represents the credentials required to connect with OpenStack servers + """ + + def __init__(self, username, password, auth_url, project_name, identity_api_version=2, image_api_version=1, + network_api_version=2, compute_api_version=2, user_domain_id='default', project_domain_id='default', + proxy_settings=None): + """ + Constructor + :param username: The user (required) + :param password: The user's password (required) + :param auth_url: The OpenStack cloud's authorization URL (required) + :param project_name: The project/tenant name + :param identity_api_version: The OpenStack's API version to use for Keystone clients + :param image_api_version: The OpenStack's API version to use for Glance clients + :param network_api_version: The OpenStack's API version to use for Neutron clients + :param compute_api_version: The OpenStack's API version to use for Nova clients + :param user_domain_id: Used for v3 APIs + :param project_domain_id: Used for v3 APIs + :param proxy_settings: instance of os_credentials.ProxySettings class + """ + self.username = username + self.password = password + self.auth_url = auth_url + self.project_name = project_name + self.identity_api_version = identity_api_version + self.image_api_version = image_api_version + self.network_api_version = network_api_version + self.compute_api_version = compute_api_version + self.user_domain_id = user_domain_id + self.project_domain_id = project_domain_id + self.proxy_settings = proxy_settings + + if self.proxy_settings and not isinstance(self.proxy_settings, ProxySettings): + raise Exception('proxy_settings must be an instance of the class ProxySettings') + + if self.auth_url: + auth_url_tokens = self.auth_url.split('/') + last_token = auth_url_tokens[len(auth_url_tokens) - 1] + if len(last_token) == 0: + last_token = auth_url_tokens[len(auth_url_tokens) - 2] + + if not last_token.startswith('v'): + raise Exception('auth_url last toke must start with \'v\'') + + def __str__(self): + """Converts object to a string""" + return 'OSCreds - username=' + str(self.username) + \ + ', password=' + str(self.password) + \ + ', auth_url=' + str(self.auth_url) + \ + ', project_name=' + str(self.project_name) + \ + ', identity_api_version=' + str(self.identity_api_version) + \ + ', image_api_version=' + str(self.image_api_version) + \ + ', network_api_version=' + str(self.network_api_version) + \ + ', compute_api_version=' + str(self.compute_api_version) + \ + ', user_domain_id=' + str(self.user_domain_id) + \ + ', proxy_settings=' + str(self.proxy_settings) + + +class ProxySettings: + """ + Represents the information required for sending traffic (HTTP & SSH) through a proxy + """ + + def __init__(self, host, port, ssh_proxy_cmd=None): + """ + Constructor + :param host: the HTTP proxy host + :param port: the HTTP proxy port + :param ssh_proxy_cmd: the SSH proxy command string (optional) + """ + # TODO - Add necessary fields here when adding support for secure proxies + + self.host = host + self.port = port + self.ssh_proxy_cmd = ssh_proxy_cmd + + if not self.host and not self.port: + raise Exception('host & port are required') + + def __str__(self): + """Converts object to a string""" + return 'ProxySettings - host=' + str(self.host) + \ + ', port=' + str(self.port) + \ + ', ssh_proxy_cmd=' + str(self.ssh_proxy_cmd) diff --git a/snaps/openstack/tests/__init__.py b/snaps/openstack/tests/__init__.py new file mode 100644 index 0000000..e3e876e --- /dev/null +++ b/snaps/openstack/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' diff --git a/snaps/openstack/tests/conf/os_env.yaml.template b/snaps/openstack/tests/conf/os_env.yaml.template new file mode 100644 index 0000000..e88bfaa --- /dev/null +++ b/snaps/openstack/tests/conf/os_env.yaml.template @@ -0,0 +1,17 @@ +# Keystone v2.0 +#username: admin +#password: admin +#os_auth_url: http://<host>:<port>/v2.0/ +#project_name: admin +#ext_net: <external network name> +#http_proxy: <host>:<port> +#ssh_proxy_cmd: '/usr/local/bin/corkscrew <host> <port> %h %p' +#ssh_proxy_cmd: 'ssh <host> nc %h %p' + +# Keystone v2.0 +#username: admin +#password: admin +#os_auth_url: http://<host>:<port>/v3 +#project_name: admin +#identity_api_version: 3 +#ext_net: <external network name>
\ No newline at end of file diff --git a/snaps/openstack/tests/conf/overcloudrc_test b/snaps/openstack/tests/conf/overcloudrc_test new file mode 100644 index 0000000..87746d8 --- /dev/null +++ b/snaps/openstack/tests/conf/overcloudrc_test @@ -0,0 +1,9 @@ +export NOVA_VERSION=1.1 +export OS_PASSWORD=test_pw +export OS_AUTH_URL=http://foo:5000/v2.0/ +export OS_USERNAME=admin +export OS_TENANT_NAME=admin +export COMPUTE_API_VERSION=1.1 +export OS_NO_CACHE=True +export OS_CLOUDNAME=undercloud +export OS_IMAGE_API_VERSION=1
\ No newline at end of file diff --git a/snaps/openstack/tests/create_flavor_tests.py b/snaps/openstack/tests/create_flavor_tests.py new file mode 100644 index 0000000..7660665 --- /dev/null +++ b/snaps/openstack/tests/create_flavor_tests.py @@ -0,0 +1,319 @@ +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack import create_flavor +from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + + +class FlavorSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the FlavorSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + FlavorSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + FlavorSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo'}) + + def test_name_ram_only(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1) + + def test_config_with_name_ram_only(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1}) + + def test_name_ram_disk_only(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=1) + + def test_config_with_name_ram_disk_only(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 1}) + + def test_ram_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram='bar', disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_ram_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 'bar', 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_ram_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1.5, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, is_public=False) + + def test_config_ram_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1.5, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_disk_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk='bar', vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_disk_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 'bar', 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_disk_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2.5, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, is_public=False) + + def test_config_disk_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2.5, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_vcpus_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus='bar', ephemeral=4, swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_vcpus_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 'bar', 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_ephemeral_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral='bar', swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_ephemeral_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 'bar', 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_ephemeral_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4.5, swap=5, rxtx_factor=6.0, is_public=False) + + def test_config_ephemeral_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4.5, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_swap_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap='bar', rxtx_factor=6.0, + is_public=False) + + def test_config_swap_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 'bar', + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_swap_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5.5, rxtx_factor=6.0, is_public=False) + + def test_config_swap_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5.5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_rxtx_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor='bar', is_public=False) + + def test_config_rxtx_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 'bar', 'is_public': False}) + + def test_is_pub_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, is_public='bar') + + def test_config_is_pub_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': 'bar'}) + + def test_name_ram_disk_vcpus_only(self): + settings = FlavorSettings(name='foo', ram=1, disk=2, vcpus=3) + self.assertEquals('foo', settings.name) + self.assertEquals('auto', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(0, settings.ephemeral) + self.assertEquals(0, settings.swap) + self.assertEquals(1.0, settings.rxtx_factor) + self.assertEquals(True, settings.is_public) + self.assertEquals(create_flavor.DEFAULT_METADATA, settings.metadata) + + def test_config_with_name_ram_disk_vcpus_only(self): + settings = FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3}) + self.assertEquals('foo', settings.name) + self.assertEquals('auto', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(0, settings.ephemeral) + self.assertEquals(0, settings.swap) + self.assertEquals(1.0, settings.rxtx_factor) + self.assertEquals(True, settings.is_public) + self.assertEquals(create_flavor.DEFAULT_METADATA, settings.metadata) + + def test_all(self): + metadata = {'foo': 'bar'} + settings = FlavorSettings(name='foo', flavor_id='bar', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5, + rxtx_factor=6.0, is_public=False, metadata=metadata) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(4, settings.ephemeral) + self.assertEquals(5, settings.swap) + self.assertEquals(6.0, settings.rxtx_factor) + self.assertEquals(False, settings.is_public) + self.assertEquals(metadata, settings.metadata) + + def test_config_all(self): + metadata = {'foo': 'bar'} + settings = FlavorSettings(config={'name': 'foo', 'flavor_id': 'bar', 'ram': 1, 'disk': 2, 'vcpus': 3, + 'ephemeral': 4, 'swap': 5, 'rxtx_factor': 6.0, 'is_public': False, + 'metadata': metadata}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(4, settings.ephemeral) + self.assertEquals(5, settings.swap) + self.assertEquals(6.0, settings.rxtx_factor) + self.assertEquals(False, settings.is_public) + self.assertEquals(metadata, settings.metadata) + + +class CreateFlavorTests(OSComponentTestCase): + """ + Test for the CreateSecurityGroup class defined in create_security_group.py + """ + + def setUp(self): + """ + Instantiates the CreateSecurityGroup object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.flavor_name = guid + 'name' + + self.nova = nova_utils.nova_client(self.os_creds) + + # Initialize for cleanup + self.flavor_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.flavor_creator: + self.flavor_creator.clean() + + def test_create_flavor(self): + """ + Tests the creation of an OpenStack flavor. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + def test_create_flavor_existing(self): + """ + Tests the creation of an OpenStack flavor then starts another creator to ensure it has not been done twice. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + flavor_creator_2 = OpenStackFlavor(self.os_creds, flavor_settings) + flavor2 = flavor_creator_2.create() + + self.assertEquals(flavor.id, flavor2.id) + + def test_create_clean_flavor(self): + """ + Tests the creation and cleanup of an OpenStack flavor. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + # Clean Flavor + self.flavor_creator.clean() + + self.assertIsNone(self.flavor_creator.get_flavor()) + self.assertIsNone(nova_utils.get_flavor_by_name(self.nova, flavor_settings.name)) + + def test_create_delete_flavor(self): + """ + Tests the creation of an OpenStack Security Group, the deletion, then cleanup to ensure clean() does not + raise any exceptions. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + # Delete Flavor + nova_utils.delete_flavor(self.nova, flavor) + self.assertIsNone(nova_utils.get_flavor_by_name(self.nova, flavor_settings.name)) + + # Attempt to cleanup + self.flavor_creator.clean() + + self.assertIsNone(self.flavor_creator.get_flavor()) + + # TODO - Add more tests to exercise all configuration options + + +def validate_flavor(flavor_settings, flavor): + """ + Validates the flavor_settings against the OpenStack flavor object + :param flavor_settings: the settings used to create the flavor + :param flavor: the OpenStack flavor object + """ + return flavor is not None \ + and flavor_settings.name == flavor.name \ + and flavor_settings.ram == flavor.ram \ + and flavor_settings.disk == flavor.disk \ + and flavor_settings.vcpus == flavor.vcpus diff --git a/snaps/openstack/tests/create_image_tests.py b/snaps/openstack/tests/create_image_tests.py new file mode 100644 index 0000000..24bf0f2 --- /dev/null +++ b/snaps/openstack/tests/create_image_tests.py @@ -0,0 +1,362 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import shutil +import uuid +import unittest + +from snaps import file_utils +from snaps.openstack.create_image import ImageSettings + +import openstack_tests +from snaps.openstack.utils import glance_utils, nova_utils +from snaps.openstack import create_image +from snaps.openstack import os_credentials +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase + +__author__ = 'spisarski' + + +class ImageSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the ImageSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + ImageSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + ImageSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo'}) + + def test_name_user_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo', image_user='bar') + + def test_config_with_name_user_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo', 'image_user': 'bar'}) + + def test_name_user_format_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo', image_user='bar', img_format='qcow2') + + def test_config_with_name_user_format_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2'}) + + def test_name_user_format_url_file_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com', + image_file='/foo/bar.qcow') + + def test_config_with_name_user_format_url_file_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'download_url': 'http://foo.com', 'image_file': '/foo/bar.qcow'}) + + def test_name_user_format_url_only(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_config_with_name_user_format_url_only(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'download_url': 'http://foo.com'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_name_user_format_file_only(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', image_file='/foo/bar.qcow') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_config_with_name_user_format_file_only(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'image_file': '/foo/bar.qcow'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_all_url(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com', + nic_config_pb_loc='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + def test_config_all_url(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'download_url': 'http://foo.com', 'nic_config_pb_loc': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + def test_all_file(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', image_file='/foo/bar.qcow', + nic_config_pb_loc='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + def test_config_all_file(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'image_file': '/foo/bar.qcow', 'nic_config_pb_loc': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + +class CreateImageSuccessTests(OSIntegrationTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = uuid.uuid4() + self.image_name = self.__class__.__name__ + '-' + str(guid) + + self.nova = nova_utils.nova_client(self.os_creds) + self.glance = glance_utils.glance_client(self.os_creds) + + self.tmp_dir = 'tmp/' + str(guid) + if not os.path.exists(self.tmp_dir): + os.makedirs(self.tmp_dir) + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.image_creator: + self.image_creator.clean() + + if os.path.exists(self.tmp_dir) and os.path.isdir(self.tmp_dir): + shutil.rmtree(self.tmp_dir) + + super(self.__class__, self).__clean__() + + def test_create_image_clean_url(self): + """ + Tests the creation of an OpenStack image from a URL. + """ + # Create Image + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings) + + created_image = self.image_creator.create() + self.assertIsNotNone(created_image) + + retrieved_image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + self.assertIsNotNone(retrieved_image) + + self.assertEquals(created_image.name, retrieved_image.name) + self.assertEquals(created_image.id, retrieved_image.id) + + def test_create_image_clean_file(self): + """ + Tests the creation of an OpenStack image from a file. + """ + url_image_settings = openstack_tests.cirros_url_image('foo') + image_file = file_utils.download(url_image_settings.url, self.tmp_dir) + file_image_settings = openstack_tests.file_image_test_settings(name=self.image_name, file_path=image_file.name) + self.image_creator = create_image.OpenStackImage(self.os_creds, file_image_settings) + + self.image = self.image_creator.create() + self.assertIsNotNone(self.image) + self.assertEqual(self.image_name, self.image.name) + + created_image = self.image_creator.create() + self.assertIsNotNone(created_image) + + retrieved_image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name) + self.assertIsNotNone(retrieved_image) + + self.assertEquals(created_image.name, retrieved_image.name) + self.assertEquals(created_image.id, retrieved_image.id) + + def test_create_delete_image(self): + """ + Tests the creation then deletion of an OpenStack image to ensure clean() does not raise an Exception. + """ + # Create Image + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings) + created_image = self.image_creator.create() + self.assertIsNotNone(created_image) + + # Delete Image manually + glance_utils.delete_image(self.glance, created_image) + + self.assertIsNone(glance_utils.get_image(self.nova, self.glance, self.image_creator.image_settings.name)) + + # Must not throw an exception when attempting to cleanup non-existent image + self.image_creator.clean() + self.assertIsNone(self.image_creator.get_image()) + + def test_create_same_image(self): + """ + Tests the creation of an OpenStack image when the image already exists. + """ + # Create Image + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings) + image1 = self.image_creator.create() + # Should be retrieving the instance data + os_image_2 = create_image.OpenStackImage(self.os_creds, os_image_settings) + image2 = os_image_2.create() + self.assertEquals(image1.id, image2.id) + + +class CreateImageNegativeTests(OSIntegrationTestCase): + """ + Negative test cases for the CreateImage class + """ + + def setUp(self): + super(self.__class__, self).__start__() + + self.image_name = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.image_creator = None + + def tearDown(self): + if self.image_creator: + self.image_creator.clean() + + super(self.__class__, self).__clean__() + + def test_none_image_name(self): + """ + Expect an exception when the image name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + self.os_creds, create_image.ImageSettings( + name=None, image_user=os_image_settings.image_user, img_format=os_image_settings.format, + url=os_image_settings.url)) + + self.fail('Exception should have been thrown prior to this line') + + def test_bad_image_url(self): + """ + Expect an exception when the image download url is bad + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, create_image.ImageSettings( + name=os_image_settings.name, image_user=os_image_settings.image_user, + img_format=os_image_settings.format, url="http://foo.bar")) + with self.assertRaises(Exception): + self.image_creator.create() + + def test_bad_image_file(self): + """ + Expect an exception when the image file does not exist + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage( + self.os_creds, + create_image.ImageSettings(name=os_image_settings.name, image_user=os_image_settings.image_user, + img_format=os_image_settings.format, image_file="/foo/bar.qcow")) + with self.assertRaises(Exception): + self.image_creator.create() + + def test_none_proj_name(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(self.os_creds.username, self.os_creds.password, self.os_creds.auth_url, None, + proxy_settings=self.os_creds.proxy_settings), + os_image_settings) + self.image_creator.create() + + def test_none_auth_url(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(self.os_creds.username, self.os_creds.password, None, + self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings), + os_image_settings) + self.image_creator.create() + + def test_none_password(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(self.os_creds.username, None, self.os_creds.os_auth_url, + self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings), + os_image_settings) + + def test_none_user(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(None, self.os_creds.password, self.os_creds.os_auth_url, + self.os_creds.project_name, + proxy_settings=self.os_creds.proxy_settings), + os_image_settings) diff --git a/snaps/openstack/tests/create_instance_tests.py b/snaps/openstack/tests/create_instance_tests.py new file mode 100644 index 0000000..756b45f --- /dev/null +++ b/snaps/openstack/tests/create_instance_tests.py @@ -0,0 +1,1474 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os +import time +import unittest +import uuid + +from snaps.openstack.create_instance import VmInstanceSettings, OpenStackVmInstance, FloatingIpSettings +from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings +from snaps.openstack.create_keypairs import OpenStackKeypair, KeypairSettings +from snaps.openstack.create_network import OpenStackNetwork, PortSettings +from snaps.openstack.create_router import OpenStackRouter +from snaps.openstack.create_image import OpenStackImage +from snaps.openstack.create_security_group import SecurityGroupSettings, OpenStackSecurityGroup +from snaps.openstack.tests import openstack_tests, validation_utils +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase, OSIntegrationTestCase + +__author__ = 'spisarski' + +VM_BOOT_TIMEOUT = 600 + +logger = logging.getLogger('create_instance_tests') + + +class VmInstanceSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the VmInstanceSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + VmInstanceSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + VmInstanceSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + VmInstanceSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + VmInstanceSettings(config={'name': 'foo'}) + + def test_name_flavor_only(self): + settings = VmInstanceSettings(name='foo', flavor='bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(0, len(settings.port_settings)) + self.assertEquals(0, len(settings.security_group_names)) + self.assertEquals(0, len(settings.floating_ip_settings)) + self.assertIsNone(settings.sudo_user) + self.assertEquals(900, settings.vm_boot_timeout) + self.assertEquals(300, settings.vm_delete_timeout) + self.assertEquals(180, settings.ssh_connect_timeout) + self.assertIsNone(settings.availability_zone) + + def test_config_with_name_flavor_only(self): + settings = VmInstanceSettings(config={'name': 'foo', 'flavor': 'bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(0, len(settings.port_settings)) + self.assertEquals(0, len(settings.security_group_names)) + self.assertEquals(0, len(settings.floating_ip_settings)) + self.assertIsNone(settings.sudo_user) + self.assertEquals(900, settings.vm_boot_timeout) + self.assertEquals(300, settings.vm_delete_timeout) + self.assertEquals(180, settings.ssh_connect_timeout) + self.assertIsNone(settings.availability_zone) + + def test_all(self): + port_settings = PortSettings(name='foo-port', network_name='bar-net') + fip_settings = FloatingIpSettings(name='foo-fip', port_name='bar-port', router_name='foo-bar-router') + + settings = VmInstanceSettings(name='foo', flavor='bar', port_settings=[port_settings], + security_group_names=['sec_grp_1'], floating_ip_settings=[fip_settings], + sudo_user='joe', vm_boot_timeout=999, vm_delete_timeout=333, + ssh_connect_timeout=111, availability_zone='server name') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(1, len(settings.port_settings)) + self.assertEquals('foo-port', settings.port_settings[0].name) + self.assertEquals('bar-net', settings.port_settings[0].network_name) + self.assertEquals(1, len(settings.security_group_names)) + self.assertEquals('sec_grp_1', settings.security_group_names[0]) + self.assertEquals(1, len(settings.floating_ip_settings)) + self.assertEquals('foo-fip', settings.floating_ip_settings[0].name) + self.assertEquals('bar-port', settings.floating_ip_settings[0].port_name) + self.assertEquals('foo-bar-router', settings.floating_ip_settings[0].router_name) + self.assertEquals('joe', settings.sudo_user) + self.assertEquals(999, settings.vm_boot_timeout) + self.assertEquals(333, settings.vm_delete_timeout) + self.assertEquals(111, settings.ssh_connect_timeout) + self.assertEquals('server name', settings.availability_zone) + + def test_config_all(self): + port_settings = PortSettings(name='foo-port', network_name='bar-net') + fip_settings = FloatingIpSettings(name='foo-fip', port_name='bar-port', router_name='foo-bar-router') + + settings = VmInstanceSettings(config={'name': 'foo', 'flavor': 'bar', 'ports': [port_settings], + 'security_group_names': ['sec_grp_1'], + 'floating_ips': [fip_settings], 'sudo_user': 'joe', + 'vm_boot_timeout': 999, 'vm_delete_timeout': 333, + 'ssh_connect_timeout': 111, 'availability_zone': 'server name'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(1, len(settings.port_settings)) + self.assertEquals('foo-port', settings.port_settings[0].name) + self.assertEquals('bar-net', settings.port_settings[0].network_name) + self.assertEquals(1, len(settings.security_group_names)) + self.assertEquals(1, len(settings.floating_ip_settings)) + self.assertEquals('foo-fip', settings.floating_ip_settings[0].name) + self.assertEquals('bar-port', settings.floating_ip_settings[0].port_name) + self.assertEquals('foo-bar-router', settings.floating_ip_settings[0].router_name) + self.assertEquals('joe', settings.sudo_user) + self.assertEquals(999, settings.vm_boot_timeout) + self.assertEquals(333, settings.vm_delete_timeout) + self.assertEquals(111, settings.ssh_connect_timeout) + self.assertEquals('server name', settings.availability_zone) + + +class FloatingIpSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the FloatingIpSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + FloatingIpSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + FloatingIpSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(config={'name': 'foo'}) + + def test_name_port_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(name='foo', port_name='bar') + + def test_config_with_name_port_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(config={'name': 'foo', 'port_name': 'bar'}) + + def test_name_router_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(name='foo', router_name='bar') + + def test_config_with_name_router_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(config={'name': 'foo', 'router_name': 'bar'}) + + def test_name_port_router_only(self): + settings = FloatingIpSettings(name='foo', port_name='foo-port', router_name='bar-router') + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertIsNone(settings.subnet_name) + self.assertTrue(settings.provisioning) + + def test_config_with_name_port_router_only(self): + settings = FloatingIpSettings(config={'name': 'foo', 'port_name': 'foo-port', 'router_name': 'bar-router'}) + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertIsNone(settings.subnet_name) + self.assertTrue(settings.provisioning) + + def test_all(self): + settings = FloatingIpSettings(name='foo', port_name='foo-port', router_name='bar-router', + subnet_name='bar-subnet', provisioning=False) + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertEquals('bar-subnet', settings.subnet_name) + self.assertFalse(settings.provisioning) + + def test_config_all(self): + settings = FloatingIpSettings(config={'name': 'foo', 'port_name': 'foo-port', 'router_name': 'bar-router', + 'subnet_name': 'bar-subnet', 'provisioning': False}) + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertEquals('bar-subnet', settings.subnet_name) + self.assertFalse(settings.provisioning) + + +class SimpleHealthCheck(OSIntegrationTestCase): + """ + Test for the CreateInstance class with a single NIC/Port with Floating IPs + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.keypair_priv_filepath = 'tmp/' + guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = guid + '-kp' + self.vm_inst_name = guid + '-inst' + self.port_1_name = guid + 'port-1' + self.port_2_name = guid + 'port-2' + self.floating_ip_name = guid + 'fip1' + + # Initialize for tearDown() + self.image_creator = None + self.network_creator = None + self.flavor_creator = None + self.inst_creator = None + + self.priv_net_config = openstack_tests.get_priv_net_config( + net_name=guid + '-priv-net', subnet_name=guid + '-priv-subnet') + self.port_settings = PortSettings( + name=self.port_1_name, network_name=self.priv_net_config.network_settings.name) + + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.priv_net_config.network_settings) + self.network_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=1024, disk=10, vcpus=1)) + self.flavor_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_check_vm_ip_dhcp(self): + """ + Tests the creation of an OpenStack instance with a single port and ensures that it's assigned IP address is + the actual. + """ + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[self.port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm = self.inst_creator.create() + + ip = self.inst_creator.get_port_ip(self.port_settings.name) + self.assertIsNotNone(ip) + + self.assertTrue(self.inst_creator.vm_active(block=True)) + + found = False + timeout = 100 + start_time = time.time() + match_value = 'Lease of ' + ip + ' obtained,' + + while timeout > time.time() - start_time: + output = vm.get_console_output() + if match_value in output: + found = True + break + self.assertTrue(found) + + +class CreateInstanceSimpleTests(OSIntegrationTestCase): + """ + Simple instance creation tests without any other objects + """ + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = guid + '-inst' + self.nova = nova_utils.nova_client(self.os_creds) + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + # Initialize for tearDown() + self.image_creator = None + self.flavor_creator = None + self.inst_creator = None + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_create_delete_instance(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP without a Floating IP. + """ + instance_settings = VmInstanceSettings(name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + + self.inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings) + + vm_inst = self.inst_creator.create() + self.assertEquals(1, len(nova_utils.get_servers_by_name(self.nova, instance_settings.name))) + + # Delete instance + nova_utils.delete_vm_instance(self.nova, vm_inst) + + self.assertTrue(self.inst_creator.vm_deleted(block=True)) + self.assertEquals(0, len(nova_utils.get_servers_by_name(self.nova, instance_settings.name))) + + # Exception should not be thrown + self.inst_creator.clean() + + +class CreateInstanceSingleNetworkTests(OSIntegrationTestCase): + """ + Test for the CreateInstance class with a single NIC/Port with Floating IPs + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.keypair_priv_filepath = 'tmp/' + guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = guid + '-kp' + self.vm_inst_name = guid + '-inst' + self.port_1_name = guid + 'port-1' + self.port_2_name = guid + 'port-2' + self.floating_ip_name = guid + 'fip1' + + # Initialize for tearDown() + self.image_creator = None + self.network_creator = None + self.router_creator = None + self.flavor_creator = None + self.keypair_creator = None + self.inst_creators = list() + + self.pub_net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings) + self.network_creator.create() + + # Create Router + self.router_creator = OpenStackRouter(self.os_creds, self.pub_net_config.router_settings) + self.router_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings( + name=self.keypair_name, public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + for inst_creator in self.inst_creators: + try: + inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.keypair_creator: + try: + self.keypair_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning keypair with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.router_creator: + try: + self.router_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning router with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_single_port_static(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP without a Floating IP. + """ + ip_1 = '10.55.1.100' + + port_settings = PortSettings( + name=self.port_1_name, network_name=self.pub_net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.pub_net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings], + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)]) + + inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + self.inst_creators.append(inst_creator) + vm_inst = inst_creator.create() + + self.assertEquals(ip_1, inst_creator.get_port_ip(self.port_1_name)) + self.assertTrue(inst_creator.vm_active(block=True)) + self.assertEquals(vm_inst, inst_creator.get_vm_inst()) + + def test_ssh_client_fip_before_active(self): + """ + Tests the ability to access a VM via SSH and a floating IP when it has been assigned prior to being active. + """ + port_settings = PortSettings( + name=self.port_1_name, network_name=self.pub_net_config.network_settings.name) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings], + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)]) + + inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + self.inst_creators.append(inst_creator) + vm_inst = inst_creator.create() + self.assertIsNotNone(vm_inst) + + self.assertTrue(inst_creator.vm_active(block=True)) + self.assertEquals(vm_inst, inst_creator.get_vm_inst()) + + validate_ssh_client(inst_creator) + + def test_ssh_client_fip_after_active(self): + """ + Tests the ability to access a VM via SSH and a floating IP when it has been assigned prior to being active. + """ + port_settings = PortSettings( + name=self.port_1_name, network_name=self.pub_net_config.network_settings.name) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings], + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)]) + + inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + self.inst_creators.append(inst_creator) + + # block=True will force the create() method to block until the + vm_inst = inst_creator.create(block=True) + self.assertIsNotNone(vm_inst) + + self.assertTrue(inst_creator.vm_active(block=True)) + self.assertEquals(vm_inst, inst_creator.get_vm_inst()) + + validate_ssh_client(inst_creator) + + # TODO - Determine how allowed_address_pairs is supposed to operate before continuing this test + # see http://docs.openstack.org/developer/dragonflow/specs/allowed_address_pairs.html for a functional description + # def test_allowed_address_port_access(self): + # """ + # Tests to ensure that setting allowed_address_pairs on a port functions as designed + # """ + # port_settings_1 = PortSettings( + # name=self.port_1_name + '-1', network_name=self.pub_net_config.network_settings.name) + # + # instance_settings_1 = VmInstanceSettings( + # name=self.vm_inst_name + '-1', flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings_1], + # floating_ip_settings=[FloatingIpSettings( + # name=self.floating_ip_name + '-1', port_name=port_settings_1.name, + # router_name=self.pub_net_config.router_settings.name)]) + # + # inst_creator_1 = OpenStackVmInstance( + # self.os_creds, instance_settings_1, self.image_creator.image_settings, + # keypair_settings=self.keypair_creator.keypair_settings) + # self.inst_creators.append(inst_creator_1) + # + # # block=True will force the create() method to block until the + # vm_inst_1 = inst_creator_1.create(block=True) + # self.assertIsNotNone(vm_inst_1) + # + # port_settings_1 = PortSettings( + # name=self.port_1_name + '-1', network_name=self.pub_net_config.network_settings.name) + # + # instance_settings_1 = VmInstanceSettings( + # name=self.vm_inst_name + '-1', flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings_1], + # floating_ip_settings=[FloatingIpSettings( + # name=self.floating_ip_name + '-1', port_name=port_settings_1.name, + # router_name=self.pub_net_config.router_settings.name)]) + # + # inst_creator_1 = OpenStackVmInstance( + # self.os_creds, instance_settings_1, self.image_creator.image_settings, + # keypair_settings=self.keypair_creator.keypair_settings) + # self.inst_creators.append(inst_creator_1) + # inst_creator_1.create(block=True) + # + # ip = inst_creator_1.get_port_ip(port_settings_1.name, + # subnet_name=self.pub_net_config.network_settings.subnet_settings[0].name) + # self.assertIsNotNone(ip) + # mac_addr = inst_creator_1.get_port_mac(port_settings_1.name) + # self.assertIsNotNone(mac_addr) + # + # allowed_address_pairs = [{'ip_address': ip, 'mac_address': mac_addr}] + # + # # Create VM that can be accessed by vm_inst_1 + # port_settings_2 = PortSettings( + # name=self.port_1_name + '-2', network_name=self.pub_net_config.network_settings.name, + # allowed_address_pairs=allowed_address_pairs) + # + # instance_settings_2 = VmInstanceSettings( + # name=self.vm_inst_name + '-2', flavor=self.flavor_creator.flavor_settings.name, + # port_settings=[port_settings_2]) + # + # inst_creator_2 = OpenStackVmInstance( + # self.os_creds, instance_settings_2, self.image_creator.image_settings) + # self.inst_creators.append(inst_creator_2) + # inst_creator_2.create(block=True) + # + # # Create VM that cannot be accessed by vm_inst_1 + # ip = '10.55.0.101' + # mac_addr = '0a:1b:2c:3d:4e:5f' + # invalid_address_pairs = [{'ip_address': ip, 'mac_address': mac_addr}] + # + # port_settings_3 = PortSettings( + # name=self.port_1_name + '-3', network_name=self.pub_net_config.network_settings.name, + # allowed_address_pairs=invalid_address_pairs) + # + # instance_settings_3 = VmInstanceSettings( + # name=self.vm_inst_name + '-3', flavor=self.flavor_creator.flavor_settings.name, + # port_settings=[port_settings_3]) + # + # inst_creator_3 = OpenStackVmInstance( + # self.os_creds, instance_settings_3, self.image_creator.image_settings) + # self.inst_creators.append(inst_creator_3) + # inst_creator_3.create(block=True) + # + # print 'foo' + # I expected that this feature would block/allow traffic from specific endpoints (VMs). In this case, I would expect + # inst_1 to be able to access inst_2 but not inst_3; however, they all can access each other. + # TODO - Add validation + + +class CreateInstancePortManipulationTests(OSIntegrationTestCase): + """ + Test for the CreateInstance class with a single NIC/Port where mac and IP values are manually set + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = guid + '-inst' + self.port_1_name = guid + 'port-1' + self.port_2_name = guid + 'port-2' + self.floating_ip_name = guid + 'fip1' + + # Initialize for tearDown() + self.image_creator = None + self.network_creator = None + self.flavor_creator = None + self.inst_creator = None + + self.net_config = openstack_tests.get_priv_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.network_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_set_custom_valid_ip_one_subnet(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP on a network with one subnet. + """ + ip = '10.55.0.101' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + self.assertEquals(ip, self.inst_creator.get_port_ip( + self.port_1_name, subnet_name=self.net_config.network_settings.subnet_settings[0].name)) + + def test_set_custom_invalid_ip_one_subnet(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP on a network with one subnet. + """ + ip = '10.66.0.101' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + + with self.assertRaises(Exception): + self.inst_creator.create() + + def test_set_custom_valid_mac(self): + """ + Tests the creation of an OpenStack instance with a single port where the MAC address is assigned. + """ + mac_addr = '0a:1b:2c:3d:4e:5f' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address=mac_addr) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + self.assertEquals(mac_addr, self.inst_creator.get_port_mac(self.port_1_name)) + + def test_set_custom_invalid_mac(self): + """ + Tests the creation of an OpenStack instance with a single port where an invalid MAC address value is being + assigned. This should raise an Exception + """ + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address='foo') + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings) + + with self.assertRaises(Exception): + self.inst_creator.create() + + def test_set_custom_mac_and_ip(self): + """ + Tests the creation of an OpenStack instance with a single port where the IP and MAC address is assigned. + """ + ip = '10.55.0.101' + mac_addr = '0a:1b:2c:3d:4e:5f' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address=mac_addr, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + self.assertEquals(ip, self.inst_creator.get_port_ip( + self.port_1_name, subnet_name=self.net_config.network_settings.subnet_settings[0].name)) + self.assertEquals(mac_addr, self.inst_creator.get_port_mac(self.port_1_name)) + + def test_set_allowed_address_pairs(self): + """ + Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set. + """ + ip = '10.55.0.101' + mac_addr = '0a:1b:2c:3d:4e:5f' + pair = {'ip_address': ip, 'mac_address': mac_addr} + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + port = self.inst_creator.get_port_by_name(port_settings.name) + self.assertIsNotNone(port) + self.assertIsNotNone(port['port'].get('allowed_address_pairs')) + self.assertEquals(1, len(port['port']['allowed_address_pairs'])) + validation_utils.objects_equivalent(pair, port['port']['allowed_address_pairs'][0]) + + def test_set_allowed_address_pairs_bad_mac(self): + """ + Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set with an + invalid MAC address. + """ + ip = '10.55.0.101' + mac_addr = 'foo' + pair = {'ip_address': ip, 'mac_address': mac_addr} + pairs = set() + pairs.add((ip, mac_addr)) + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + with self.assertRaises(Exception): + self.inst_creator.create() + + def test_set_allowed_address_pairs_bad_ip(self): + """ + Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set with an + invalid MAC address. + """ + ip = 'foo' + mac_addr = '0a:1b:2c:3d:4e:5f' + pair = {'ip_address': ip, 'mac_address': mac_addr} + pairs = set() + pairs.add((ip, mac_addr)) + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + with self.assertRaises(Exception): + self.inst_creator.create() + + +class CreateInstanceOnComputeHost(OSComponentTestCase): + """ + Test for the CreateInstance where one VM is deployed to each compute node + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = guid + '-inst' + self.port_base_name = guid + 'port' + + # Initialize for tearDown() + self.image_creator = None + self.flavor_creator = None + self.network_creator = None + self.inst_creators = list() + + self.priv_net_config = openstack_tests.get_priv_net_config( + net_name=guid + '-priv-net', subnet_name=guid + '-priv-subnet') + + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.priv_net_config.network_settings) + self.network_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=512, disk=1, vcpus=1)) + self.flavor_creator.create() + + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + for inst_creator in self.inst_creators: + try: + inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + def test_deploy_vm_to_each_compute_node(self): + """ + Tests the creation of OpenStack VM instances to each compute node. + """ + from snaps.openstack.utils import nova_utils + nova = nova_utils.nova_client(self.os_creds) + zones = nova_utils.get_nova_availability_zones(nova) + + # Create Instance on each server/zone + ctr = 0 + for zone in zones: + inst_name = self.vm_inst_name + '-' + zone + ctr += 1 + port_settings = PortSettings(name=self.port_base_name + '-' + str(ctr), + network_name=self.priv_net_config.network_settings.name) + + instance_settings = VmInstanceSettings( + name=inst_name, flavor=self.flavor_creator.flavor_settings.name, availability_zone=zone, + port_settings=[port_settings]) + inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creators.append(inst_creator) + inst_creator.create() + + # Validate instances to ensure they've been deployed to the correct server + index = 0 + for zone in zones: + creator = self.inst_creators[index] + self.assertTrue(creator.vm_active(block=True)) + vm = creator.get_vm_inst() + deployed_zone = vm._info['OS-EXT-AZ:availability_zone'] + deployed_host = vm._info['OS-EXT-SRV-ATTR:host'] + self.assertEquals(zone, deployed_zone + ':' + deployed_host) + index += 1 + + +class CreateInstancePubPrivNetTests(OSIntegrationTestCase): + """ + Test for the CreateInstance class with two NIC/Ports, eth0 with floating IP and eth1 w/o + These tests require a Centos image + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + # Initialize for tearDown() + self.image_creator = None + self.network_creators = list() + self.router_creators = list() + self.flavor_creator = None + self.keypair_creator = None + self.inst_creator = None + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.keypair_priv_filepath = 'tmp/' + self.guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = self.guid + '-kp' + self.vm_inst_name = self.guid + '-inst' + self.port_1_name = self.guid + '-port-1' + self.port_2_name = self.guid + '-port-2' + self.floating_ip_name = self.guid + 'fip1' + self.priv_net_config = openstack_tests.get_priv_net_config( + net_name=self.guid + '-priv-net', subnet_name=self.guid + '-priv-subnet', + router_name=self.guid + '-priv-router', external_net=self.ext_net_name) + self.pub_net_config = openstack_tests.get_pub_net_config( + net_name=self.guid + '-pub-net', subnet_name=self.guid + '-pub-subnet', + router_name=self.guid + '-pub-router', external_net=self.ext_net_name) + image_name = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.os_image_settings = openstack_tests.centos_url_image(name=image_name) + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # First network is public + self.network_creators.append(OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings)) + # Second network is private + self.network_creators.append(OpenStackNetwork(self.os_creds, self.priv_net_config.network_settings)) + for network_creator in self.network_creators: + network_creator.create() + + self.router_creators.append(OpenStackRouter(self.os_creds, self.pub_net_config.router_settings)) + self.router_creators.append(OpenStackRouter(self.os_creds, self.priv_net_config.router_settings)) + + # Create Routers + for router_creator in self.router_creators: + router_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=self.guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + + # Create Keypair + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings( + name=self.keypair_name, public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + except Exception as e: + self.tearDown() + raise Exception(e.message) + + def tearDown(self): + """ + Cleans the created objects + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.keypair_creator: + try: + self.keypair_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning keypair with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + for router_creator in self.router_creators: + try: + router_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning router with message - ' + e.message) + + for network_creator in self.network_creators: + try: + network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_dual_ports_dhcp(self): + """ + Tests the creation of an OpenStack instance with a dual ports/NICs with a DHCP assigned IP. + NOTE: This test and any others that call ansible will most likely fail unless you do one of + two things: + 1. Have a ~/.ansible.cfg (or alternate means) to set host_key_checking = False + 2. Set the following environment variable in your executing shell: ANSIBLE_HOST_KEY_CHECKING=False + Should this not be performed, the creation of the host ssh key will cause your ansible calls to fail. + """ + # Create ports/NICs for instance + ports_settings = [] + ctr = 1 + for network_creator in self.network_creators: + ports_settings.append(PortSettings( + name=self.guid + '-port-' + str(ctr), + network_name=network_creator.network_settings.name)) + ctr += 1 + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=ports_settings, + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)]) + + self.inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + + vm_inst = self.inst_creator.create(block=True) + + self.assertEquals(vm_inst, self.inst_creator.get_vm_inst()) + + # Effectively blocks until VM has been properly activated + self.assertTrue(self.inst_creator.vm_active(block=True)) + + # Effectively blocks until VM's ssh port has been opened + self.assertTrue(self.inst_creator.vm_ssh_active(block=True)) + + self.inst_creator.config_nics() + + # TODO - *** ADD VALIDATION HERE *** + # TODO - Add validation that both floating IPs work + # TODO - Add tests where only one NIC has a floating IP + # TODO - Add tests where one attempts to place a floating IP on a network/router without an external gateway + + +class InstanceSecurityGroupTests(OSIntegrationTestCase): + """ + Tests that include, add, and remove security groups from VM instances + """ + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = self.guid + '-inst' + self.nova = nova_utils.nova_client(self.os_creds) + self.os_image_settings = openstack_tests.cirros_url_image(name=self.guid + '-image') + + self.keypair_priv_filepath = 'tmp/' + self.guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = self.guid + '-kp' + self.vm_inst_name = self.guid + '-inst' + self.port_1_name = self.guid + 'port-1' + self.port_2_name = self.guid + 'port-2' + self.floating_ip_name = self.guid + 'fip1' + + self.pub_net_config = openstack_tests.get_pub_net_config( + net_name=self.guid + '-pub-net', subnet_name=self.guid + '-pub-subnet', + router_name=self.guid + '-pub-router', external_net=self.ext_net_name) + + # Initialize for tearDown() + self.image_creator = None + self.keypair_creator = None + self.flavor_creator = None + self.network_creator = None + self.router_creator = None + self.inst_creator = None + self.sec_grp_creators = list() + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings) + self.network_creator.create() + + # Create Router + self.router_creator = OpenStackRouter(self.os_creds, self.pub_net_config.router_settings) + self.router_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=self.guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings( + name=self.keypair_name, public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + for sec_grp_creator in self.sec_grp_creators: + try: + sec_grp_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning security group with message - ' + e.message) + + if self.keypair_creator: + try: + self.keypair_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning keypair with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.router_creator: + try: + self.router_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning router with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_add_security_group(self): + """ + Tests the addition of a security group created after the instance. + """ + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Check that group has not been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + # Add security group to instance after activated + self.inst_creator.add_security_group(sec_grp) + + # Validate that security group has been added + self.assertTrue(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_add_invalid_security_group(self): + """ + Tests the addition of a security group that no longer exists. + """ + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + sec_grp_creator.clean() + self.sec_grp_creators.append(sec_grp_creator) + + # Check that group has not been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertFalse(self.inst_creator.add_security_group(sec_grp)) + + # Validate that security group has been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_remove_security_group(self): + """ + Tests the removal of a security group created before and added to the instance. + """ + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, + security_group_names=[sec_grp_settings.name]) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Check that group has been added + self.assertTrue(inst_has_sec_grp(vm_inst, sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertTrue(self.inst_creator.remove_security_group(sec_grp)) + + # Validate that security group has been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_remove_security_group_never_added(self): + """ + Tests the removal of a security group that was never added in the first place. + """ + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Check that group has been added + self.assertFalse(inst_has_sec_grp(vm_inst, sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertFalse(self.inst_creator.remove_security_group(sec_grp)) + + # Validate that security group has been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_add_same_security_group(self): + """ + Tests the addition of a security group created before add added to the instance. + """ + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, + security_group_names=[sec_grp_settings.name]) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Check that group has been added + self.assertTrue(inst_has_sec_grp(vm_inst, sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertTrue(self.inst_creator.add_security_group(sec_grp)) + + # Validate that security group has been added + self.assertTrue(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + +def inst_has_sec_grp(vm_inst, sec_grp_name): + """ + Returns true if instance has a security group of a given name + :return: + """ + if not hasattr(vm_inst, 'security_groups'): + return False + + found = False + for sec_grp_dict in vm_inst.security_groups: + if sec_grp_name in sec_grp_dict['name']: + found = True + break + return found + + +def validate_ssh_client(instance_creator): + """ + Returns True if instance_creator returns an SSH client that is valid + :param instance_creator: the object responsible for creating the VM instance + :return: T/F + """ + ssh_active = instance_creator.vm_ssh_active(block=True) + + if ssh_active: + ssh_client = instance_creator.ssh_client() + if ssh_client: + out = ssh_client.exec_command('pwd')[1] + else: + return False + + channel = out.channel + in_buffer = channel.in_buffer + pwd_out = in_buffer.read(1024) + if not pwd_out or len(pwd_out) < 10: + return False + return True + + return False diff --git a/snaps/openstack/tests/create_keypairs_tests.py b/snaps/openstack/tests/create_keypairs_tests.py new file mode 100644 index 0000000..e4409a9 --- /dev/null +++ b/snaps/openstack/tests/create_keypairs_tests.py @@ -0,0 +1,203 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import uuid +import unittest + +from Crypto.PublicKey import RSA + +from snaps.openstack.create_keypairs import KeypairSettings, OpenStackKeypair +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase + +__author__ = 'spisarski' + + +class KeypairSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the KeypairSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + KeypairSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + KeypairSettings(config=dict()) + + def test_name_only(self): + settings = KeypairSettings(name='foo') + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_config_with_name_only(self): + settings = KeypairSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_name_pub_only(self): + settings = KeypairSettings(name='foo', public_filepath='/foo/bar.pub') + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_config_with_name_pub_only(self): + settings = KeypairSettings(config={'name': 'foo', 'public_filepath': '/foo/bar.pub'}) + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_name_priv_only(self): + settings = KeypairSettings(name='foo', private_filepath='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + def test_config_with_name_priv_only(self): + settings = KeypairSettings(config={'name': 'foo', 'private_filepath': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + def test_all(self): + settings = KeypairSettings(name='foo', public_filepath='/foo/bar.pub', private_filepath='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + def test_config_all(self): + settings = KeypairSettings(config={'name': 'foo', 'public_filepath': '/foo/bar.pub', + 'private_filepath': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + +class CreateKeypairsTests(OSIntegrationTestCase): + """ + Tests for the OpenStackKeypair class + """ + + def setUp(self): + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.priv_file_path = 'tmp/' + guid + self.pub_file_path = self.priv_file_path + '.pub' + self.nova = nova_utils.nova_client(self.os_creds) + self.keypair_name = guid + + self.keypair_creator = None + + def tearDown(self): + """ + Cleanup of created keypair + """ + if self.keypair_creator: + self.keypair_creator.clean() + + try: + os.remove(self.pub_file_path) + except: + pass + + try: + os.remove(self.priv_file_path) + except: + pass + + super(self.__class__, self).__clean__() + + def test_create_keypair_only(self): + """ + Tests the creation of a generated keypair without saving to file + :return: + """ + self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(name=self.keypair_name)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + def test_create_delete_keypair(self): + """ + Tests the creation then deletion of an OpenStack keypair to ensure clean() does not raise an Exception. + """ + # Create Image + self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(name=self.keypair_name)) + created_keypair = self.keypair_creator.create() + self.assertIsNotNone(created_keypair) + + # Delete Image manually + nova_utils.delete_keypair(self.nova, created_keypair) + + self.assertIsNone(nova_utils.get_keypair_by_name(self.nova, self.keypair_name)) + + # Must not throw an exception when attempting to cleanup non-existent image + self.keypair_creator.clean() + self.assertIsNone(self.keypair_creator.get_keypair()) + + def test_create_keypair_save_pub_only(self): + """ + Tests the creation of a generated keypair and saves the public key only + :return: + """ + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + file_key = open(os.path.expanduser(self.pub_file_path)).read() + self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key) + + def test_create_keypair_save_both(self): + """ + Tests the creation of a generated keypair and saves both private and public key files[ + :return: + """ + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path, + private_filepath=self.priv_file_path)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + file_key = open(os.path.expanduser(self.pub_file_path)).read() + self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key) + + self.assertTrue(os.path.isfile(self.priv_file_path)) + + def test_create_keypair_from_file(self): + """ + Tests the creation of an existing public keypair from a file + :return: + """ + keys = RSA.generate(1024) + nova_utils.save_keys_to_files(keys=keys, pub_file_path=self.pub_file_path) + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + file_key = open(os.path.expanduser(self.pub_file_path)).read() + self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key) diff --git a/snaps/openstack/tests/create_network_tests.py b/snaps/openstack/tests/create_network_tests.py new file mode 100644 index 0000000..a2b17f8 --- /dev/null +++ b/snaps/openstack/tests/create_network_tests.py @@ -0,0 +1,533 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack.create_network import OpenStackNetwork, NetworkSettings, SubnetSettings, PortSettings +from snaps.openstack import create_router +from snaps.openstack.tests import openstack_tests +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase, OSComponentTestCase +from snaps.openstack.utils import neutron_utils +from snaps.openstack.utils.tests import neutron_utils_tests + +__author__ = 'spisarski' + + +class NetworkSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the NetworkSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + NetworkSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + NetworkSettings(config=dict()) + + def test_name_only(self): + settings = NetworkSettings(name='foo') + self.assertEquals('foo', settings.name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.shared) + self.assertIsNone(settings.project_name) + self.assertFalse(settings.external) + self.assertIsNone(settings.network_type) + self.assertEquals(0, len(settings.subnet_settings)) + + def test_config_with_name_only(self): + settings = NetworkSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.shared) + self.assertIsNone(settings.project_name) + self.assertFalse(settings.external) + self.assertIsNone(settings.network_type) + self.assertEquals(0, len(settings.subnet_settings)) + + def test_all(self): + sub_settings = SubnetSettings(name='foo-subnet', cidr='10.0.0.0/24') + settings = NetworkSettings(name='foo', admin_state_up=False, shared=True, project_name='bar', external=True, + network_type='flat', physical_network='phy', subnet_settings=[sub_settings]) + self.assertEquals('foo', settings.name) + self.assertFalse(settings.admin_state_up) + self.assertTrue(settings.shared) + self.assertEquals('bar', settings.project_name) + self.assertTrue(settings.external) + self.assertEquals('flat', settings.network_type) + self.assertEquals('phy', settings.physical_network) + self.assertEquals(1, len(settings.subnet_settings)) + self.assertEquals('foo-subnet', settings.subnet_settings[0].name) + + def test_config_all(self): + settings = NetworkSettings(config={'name': 'foo', 'admin_state_up': False, 'shared': True, + 'project_name': 'bar', 'external': True, 'network_type': 'flat', + 'physical_network': 'phy', + 'subnets': + [{'subnet': {'name': 'foo-subnet', 'cidr': '10.0.0.0/24'}}]}) + self.assertEquals('foo', settings.name) + self.assertFalse(settings.admin_state_up) + self.assertTrue(settings.shared) + self.assertEquals('bar', settings.project_name) + self.assertTrue(settings.external) + self.assertEquals('flat', settings.network_type) + self.assertEquals('phy', settings.physical_network) + self.assertEquals(1, len(settings.subnet_settings)) + self.assertEquals('foo-subnet', settings.subnet_settings[0].name) + + +class SubnetSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the SubnetSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + SubnetSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + SubnetSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + SubnetSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + SubnetSettings(config={'name': 'foo'}) + + def test_name_cidr_only(self): + settings = SubnetSettings(name='foo', cidr='10.0.0.0/24') + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(4, settings.ip_version) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.start) + self.assertIsNone(settings.end) + self.assertIsNone(settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertIsNone(settings.host_routes) + self.assertIsNone(settings.destination) + self.assertIsNone(settings.nexthop) + self.assertIsNone(settings.ipv6_ra_mode) + self.assertIsNone(settings.ipv6_address_mode) + + def test_config_with_name_cidr_only(self): + settings = SubnetSettings(config={'name': 'foo', 'cidr': '10.0.0.0/24'}) + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(4, settings.ip_version) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.start) + self.assertIsNone(settings.end) + self.assertIsNone(settings.gateway_ip) + self.assertIsNone(settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertIsNone(settings.host_routes) + self.assertIsNone(settings.destination) + self.assertIsNone(settings.nexthop) + self.assertIsNone(settings.ipv6_ra_mode) + self.assertIsNone(settings.ipv6_address_mode) + + def test_all(self): + host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'} + settings = SubnetSettings(name='foo', cidr='10.0.0.0/24', ip_version=6, project_name='bar-project', + start='10.0.0.2', end='10.0.0.101', gateway_ip='10.0.0.1', enable_dhcp=False, + dns_nameservers=['8.8.8.8'], host_routes=[host_routes], destination='dest', + nexthop='hop', ipv6_ra_mode='dhcpv6-stateful', ipv6_address_mode='slaac') + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(6, settings.ip_version) + self.assertEquals('bar-project', settings.project_name) + self.assertEquals('10.0.0.2', settings.start) + self.assertEquals('10.0.0.101', settings.end) + self.assertEquals('10.0.0.1', settings.gateway_ip) + self.assertEquals(False, settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertEquals(1, len(settings.host_routes)) + self.assertEquals(host_routes, settings.host_routes[0]) + self.assertEquals('dest', settings.destination) + self.assertEquals('hop', settings.nexthop) + self.assertEquals('dhcpv6-stateful', settings.ipv6_ra_mode) + self.assertEquals('slaac', settings.ipv6_address_mode) + + def test_config_all(self): + host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'} + settings = SubnetSettings(config={'name': 'foo', 'cidr': '10.0.0.0/24', 'ip_version': 6, + 'project_name': 'bar-project', 'start': '10.0.0.2', 'end': '10.0.0.101', + 'gateway_ip': '10.0.0.1', 'enable_dhcp': False, + 'dns_nameservers': ['8.8.8.8'], 'host_routes': [host_routes], + 'destination': 'dest', 'nexthop': 'hop', 'ipv6_ra_mode': 'dhcpv6-stateful', + 'ipv6_address_mode': 'slaac'}) + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(6, settings.ip_version) + self.assertEquals('bar-project', settings.project_name) + self.assertEquals('10.0.0.2', settings.start) + self.assertEquals('10.0.0.101', settings.end) + self.assertEquals('10.0.0.1', settings.gateway_ip) + self.assertEquals(False, settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertEquals(1, len(settings.host_routes)) + self.assertEquals(host_routes, settings.host_routes[0]) + self.assertEquals('dest', settings.destination) + self.assertEquals('hop', settings.nexthop) + self.assertEquals('dhcpv6-stateful', settings.ipv6_ra_mode) + self.assertEquals('slaac', settings.ipv6_address_mode) + + +class PortSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the PortSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + PortSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + PortSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + PortSettings(name='foo') + + def test_config_name_only(self): + with self.assertRaises(Exception): + PortSettings(config={'name': 'foo'}) + + def test_name_netname_only(self): + settings = PortSettings(name='foo', network_name='bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.mac_address) + self.assertIsNone(settings.ip_addrs) + self.assertIsNone(settings.fixed_ips) + self.assertIsNone(settings.security_groups) + self.assertIsNone(settings.allowed_address_pairs) + self.assertIsNone(settings.opt_value) + self.assertIsNone(settings.opt_name) + self.assertIsNone(settings.device_owner) + self.assertIsNone(settings.device_id) + + def test_config_with_name_netname_only(self): + settings = PortSettings(config={'name': 'foo', 'network_name': 'bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.mac_address) + self.assertIsNone(settings.ip_addrs) + self.assertIsNone(settings.fixed_ips) + self.assertIsNone(settings.security_groups) + self.assertIsNone(settings.allowed_address_pairs) + self.assertIsNone(settings.opt_value) + self.assertIsNone(settings.opt_name) + self.assertIsNone(settings.device_owner) + self.assertIsNone(settings.device_id) + + def test_all(self): + ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}] + fixed_ips = {'sub_id', '10.0.0.10'} + allowed_address_pairs = {'10.0.0.101', '1234.5678'} + + settings = PortSettings(name='foo', network_name='bar', admin_state_up=False, project_name='foo-project', + mac_address='1234', ip_addrs=ip_addrs, fixed_ips=fixed_ips, + security_groups=['foo_grp_id'], allowed_address_pairs=allowed_address_pairs, + opt_value='opt value', opt_name='opt name', device_owner='owner', + device_id='device number') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertFalse(settings.admin_state_up) + self.assertEquals('foo-project', settings.project_name) + self.assertEquals('1234', settings.mac_address) + self.assertEquals(ip_addrs, settings.ip_addrs) + self.assertEquals(fixed_ips, settings.fixed_ips) + self.assertEquals(1, len(settings.security_groups)) + self.assertEquals('foo_grp_id', settings.security_groups[0]) + self.assertEquals(allowed_address_pairs, settings.allowed_address_pairs) + self.assertEquals('opt value', settings.opt_value) + self.assertEquals('opt name', settings.opt_name) + self.assertEquals('owner', settings.device_owner) + self.assertEquals('device number', settings.device_id) + + def test_config_all(self): + ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}] + fixed_ips = {'sub_id', '10.0.0.10'} + allowed_address_pairs = {'10.0.0.101', '1234.5678'} + + settings = PortSettings(config={'name': 'foo', 'network_name': 'bar', 'admin_state_up': False, + 'project_name': 'foo-project', 'mac_address': '1234', 'ip_addrs': ip_addrs, + 'fixed_ips': fixed_ips, 'security_groups': ['foo_grp_id'], + 'allowed_address_pairs': allowed_address_pairs, 'opt_value': 'opt value', + 'opt_name': 'opt name', 'device_owner': 'owner', 'device_id': 'device number'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertFalse(settings.admin_state_up) + self.assertEquals('foo-project', settings.project_name) + self.assertEquals('1234', settings.mac_address) + self.assertEquals(ip_addrs, settings.ip_addrs) + self.assertEquals(fixed_ips, settings.fixed_ips) + self.assertEquals(1, len(settings.security_groups)) + self.assertEquals('foo_grp_id', settings.security_groups[0]) + self.assertEquals(allowed_address_pairs, settings.allowed_address_pairs) + self.assertEquals('opt value', settings.opt_value) + self.assertEquals('opt name', settings.opt_name) + self.assertEquals('owner', settings.device_owner) + self.assertEquals('device number', settings.device_id) + + +class CreateNetworkSuccessTests(OSIntegrationTestCase): + """ + Test for the CreateNework class defined in create_nework.py + """ + + def setUp(self): + """ + Sets up object for test + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + + self.neutron = neutron_utils.neutron_client(self.os_creds) + + # Initialize for cleanup + self.net_creator = None + self.router_creator = None + self.neutron = neutron_utils.neutron_client(self.os_creds) + + def tearDown(self): + """ + Cleans the network + """ + if self.router_creator: + self.router_creator.clean() + + if self.net_creator: + if len(self.net_creator.get_subnets()) > 0: + # Validate subnet has been deleted + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, False) + + if self.net_creator.get_network(): + # Validate network has been deleted + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, + False) + self.net_creator.clean() + + super(self.__class__, self).__clean__() + + def test_create_network_without_router(self): + """ + Tests the creation of an OpenStack network without a router. + """ + # Create Nework + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True) + + # Validate subnets + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, True) + + def test_create_delete_network(self): + """ + Tests the creation of an OpenStack network, it's deletion, then cleanup. + """ + # Create Nework + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True) + + neutron_utils.delete_network(self.neutron, self.net_creator.get_network()) + self.assertIsNone(neutron_utils.get_network(self.neutron, self.net_creator.network_settings.name)) + + # This shall not throw an exception here + self.net_creator.clean() + + def test_create_network_with_router(self): + """ + Tests the creation of an OpenStack network with a router. + """ + # Create Network + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + # Create Router + self.router_creator = create_router.OpenStackRouter(self.os_creds, self.net_config.router_settings) + self.router_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True) + + # Validate subnets + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, True) + + # Validate routers + neutron_utils_tests.validate_router(self.neutron, self.router_creator.router_settings.name, True) + + neutron_utils_tests.validate_interface_router(self.router_creator.get_internal_router_interface(), + self.router_creator.get_router(), + self.net_creator.get_subnets()[0]) + + def test_create_networks_same_name(self): + """ + Tests the creation of an OpenStack network and ensures that the OpenStackNetwork object will not create + a second. + """ + # Create Nework + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + self.net_creator2 = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator2.create() + + self.assertEquals(self.net_creator.get_network()['network']['id'], + self.net_creator2.get_network()['network']['id']) + + +class CreateNetworkTypeTests(OSComponentTestCase): + """ + Test for the CreateNework class defined in create_nework.py for testing creating networks of different types + """ + + def setUp(self): + """ + Sets up object for test + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet') + + self.neutron = neutron_utils.neutron_client(self.os_creds) + + # Initialize for cleanup + self.net_creator = None + self.neutron = neutron_utils.neutron_client(self.os_creds) + + def tearDown(self): + """ + Cleans the network + """ + if self.net_creator: + if len(self.net_creator.get_subnets()) > 0: + # Validate subnet has been deleted + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, False) + + if self.net_creator.get_network(): + # Validate network has been deleted + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, + False) + self.net_creator.clean() + # TODO - determine why this is not working on Newton + # - Unable to create the network. No tenant network is available for allocation. + # def test_create_network_type_vlan(self): + # """ + # Tests the creation of an OpenStack network of type vlan. + # """ + # # Create Network + # network_type = 'vlan' + # net_settings = NetworkSettings(name=self.net_config.network_settings.name, + # subnet_settings=self.net_config.network_settings.subnet_settings, + # network_type=network_type) + # + # # When setting the network_type, creds must be admin + # self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + # network = self.net_creator.create() + # + # # Validate network was created + # neutron_utils_tests.validate_network(self.neutron, net_settings.name, True) + # + # self.assertEquals(network_type, network['network']['provider:network_type']) + + def test_create_network_type_vxlan(self): + """ + Tests the creation of an OpenStack network of type vxlan. + """ + # Create Network + network_type = 'vxlan' + net_settings = NetworkSettings(name=self.net_config.network_settings.name, + subnet_settings=self.net_config.network_settings.subnet_settings, + network_type=network_type) + + # When setting the network_type, creds must be admin + self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + network = self.net_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, net_settings.name, True) + + self.assertEquals(network_type, network['network']['provider:network_type']) + + # TODO - determine what value we need to place into physical_network + # - Do not know what vaule to place into the 'physical_network' setting. + # def test_create_network_type_flat(self): + # """ + # Tests the creation of an OpenStack network of type flat. + # """ + # # Create Network + # network_type = 'flat' + # + # # Unable to find documentation on how to find a value that will work here. + # # https://visibilityspots.org/vlan-flat-neutron-provider.html + # # https://community.rackspace.com/products/f/45/t/4225 + # # It appears that this may be due to how OPNFV is configuring OpenStack. + # physical_network = '???' + # net_settings = NetworkSettings(name=self.net_config.network_settings.name, + # subnet_settings=self.net_config.network_settings.subnet_settings, + # network_type=network_type, physical_network=physical_network) + # self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + # network = self.net_creator.create() + # + # # Validate network was created + # neutron_utils_tests.validate_network(self.neutron, net_settings.name, True) + # + # self.assertEquals(network_type, network['network']['provider:network_type']) + + def test_create_network_type_foo(self): + """ + Tests the creation of an OpenStack network of type foo which should raise an exception. + """ + # Create Network + network_type = 'foo' + net_settings = NetworkSettings(name=self.net_config.network_settings.name, + subnet_settings=self.net_config.network_settings.subnet_settings, + network_type=network_type) + self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + with self.assertRaises(Exception): + self.net_creator.create() diff --git a/snaps/openstack/tests/create_project_tests.py b/snaps/openstack/tests/create_project_tests.py new file mode 100644 index 0000000..9d53467 --- /dev/null +++ b/snaps/openstack/tests/create_project_tests.py @@ -0,0 +1,228 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack.create_project import OpenStackProject, ProjectSettings +from snaps.openstack.create_security_group import OpenStackSecurityGroup +from snaps.openstack.create_security_group import SecurityGroupSettings +from snaps.openstack.create_user import OpenStackUser +from snaps.openstack.create_user import UserSettings +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + + +class ProjectSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the ProjectSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + ProjectSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + ProjectSettings(config=dict()) + + def test_name_only(self): + settings = ProjectSettings(name='foo') + self.assertEquals('foo', settings.name) + self.assertEquals('default', settings.domain) + self.assertIsNone(settings.description) + self.assertTrue(settings.enabled) + + def test_config_with_name_only(self): + settings = ProjectSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + self.assertEquals('default', settings.domain) + self.assertIsNone(settings.description) + self.assertTrue(settings.enabled) + + def test_all(self): + settings = ProjectSettings(name='foo', domain='bar', description='foobar', enabled=False) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.domain) + self.assertEquals('foobar', settings.description) + self.assertFalse(settings.enabled) + + def test_config_all(self): + settings = ProjectSettings(config={'name': 'foo', 'domain': 'bar', 'description': 'foobar', 'enabled': False}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.domain) + self.assertEquals('foobar', settings.description) + self.assertFalse(settings.enabled) + + +class CreateProjectSuccessTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = str(uuid.uuid4())[:-19] + guid = self.__class__.__name__ + '-' + guid + self.project_settings = ProjectSettings(name=guid + '-name') + + self.keystone = keystone_utils.keystone_client(self.os_creds) + + # Initialize for cleanup + self.project_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.project_creator: + self.project_creator.clean() + + def test_create_project(self): + """ + Tests the creation of an OpenStack project. + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + retrieved_project = keystone_utils.get_project(keystone=self.keystone, project_name=self.project_settings.name) + self.assertIsNotNone(retrieved_project) + self.assertEquals(created_project, retrieved_project) + + def test_create_project_2x(self): + """ + Tests the creation of an OpenStack project twice to ensure it only creates one. + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + retrieved_project = keystone_utils.get_project(keystone=self.keystone, project_name=self.project_settings.name) + self.assertIsNotNone(retrieved_project) + self.assertEquals(created_project, retrieved_project) + + project2 = OpenStackProject(self.os_creds, self.project_settings).create() + self.assertEquals(retrieved_project, project2) + + def test_create_delete_project(self): + """ + Tests the creation of an OpenStack project, it's deletion, then cleanup. + """ + # Create Image + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + keystone_utils.delete_project(self.keystone, created_project) + + self.project_creator.clean() + + self.assertIsNone(self.project_creator.get_project()) + + # TODO - Expand tests + + +class CreateProjectUserTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = str(uuid.uuid4())[:-19] + self.guid = self.__class__.__name__ + '-' + guid + self.project_settings = ProjectSettings(name=self.guid + '-name') + + self.keystone = keystone_utils.keystone_client(self.os_creds) + + # Initialize for cleanup + self.project_creator = None + self.user_creators = list() + + self.sec_grp_creators = list() + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + for sec_grp_creator in self.sec_grp_creators: + sec_grp_creator.clean() + + for user_creator in self.user_creators: + user_creator.clean() + + if self.project_creator: + self.project_creator.clean() + + def test_create_project_sec_grp_one_user(self): + """ + Tests the creation of an OpenStack object to a project with a new users and to create a security group + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + user_creator = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user', password=self.guid)) + self.project_creator.assoc_user(user_creator.create()) + self.user_creators.append(user_creator) + + sec_grp_os_creds = user_creator.get_os_creds(self.project_creator.get_project().name) + sec_grp_creator = OpenStackSecurityGroup( + sec_grp_os_creds, SecurityGroupSettings(name=self.guid + '-name', description='hello group')) + sec_grp = sec_grp_creator.create() + self.assertIsNotNone(sec_grp) + self.sec_grp_creators.append(sec_grp_creator) + + if self.keystone.version == keystone_utils.V2_VERSION: + self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['tenant_id']) + else: + self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['project_id']) + + def test_create_project_sec_grp_two_users(self): + """ + Tests the creation of an OpenStack object to a project with two new users and use each user to create a + security group + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + user_creator_1 = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user1', password=self.guid)) + self.project_creator.assoc_user(user_creator_1.create()) + self.user_creators.append(user_creator_1) + + user_creator_2 = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user2', password=self.guid)) + self.project_creator.assoc_user(user_creator_2.create()) + self.user_creators.append(user_creator_2) + + ctr = 0 + for user_creator in self.user_creators: + ctr += 1 + sec_grp_os_creds = user_creator.get_os_creds(self.project_creator.get_project().name) + + sec_grp_creator = OpenStackSecurityGroup( + sec_grp_os_creds, SecurityGroupSettings(name=self.guid + '-name', description='hello group')) + sec_grp = sec_grp_creator.create() + self.assertIsNotNone(sec_grp) + self.sec_grp_creators.append(sec_grp_creator) + self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['tenant_id']) diff --git a/snaps/openstack/tests/create_router_tests.py b/snaps/openstack/tests/create_router_tests.py new file mode 100644 index 0000000..3e22714 --- /dev/null +++ b/snaps/openstack/tests/create_router_tests.py @@ -0,0 +1,264 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +from snaps.openstack import create_network +from snaps.openstack import create_router +from snaps.openstack.create_network import NetworkSettings +from snaps.openstack.create_network import OpenStackNetwork +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase +from snaps.openstack.create_router import RouterSettings +from snaps.openstack.utils import neutron_utils + +__author__ = 'mmakati' + +cidr1 = '10.200.201.0/24' +cidr2 = '10.200.202.0/24' +static_gateway_ip1 = '10.200.201.1' +static_gateway_ip2 = '10.200.202.1' + + +class CreateRouterSuccessTests(OSIntegrationTestCase): + """ + Class for testing routers with various positive scenarios expected to succeed + """ + + def setUp(self): + """ + Initializes objects used for router testing + """ + super(self.__class__, self).__start__() + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.router_creator = None + self.network_creator1 = None + self.network_creator2 = None + self.neutron = neutron_utils.neutron_client(self.os_creds) + + def tearDown(self): + """ + Cleans the remote OpenStack objects used for router testing + """ + if self.router_creator: + self.router_creator.clean() + + if self.network_creator1: + self.network_creator1.clean() + + if self.network_creator2: + self.network_creator2.clean() + + super(self.__class__, self).__clean__() + + def test_create_router_vanilla(self): + """ + Test creation of a most basic router with minimal options. + """ + router_settings = RouterSettings(name=self.guid + '-pub-router', external_gateway=self.ext_net_name) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + self.assertIsNotNone(router) + + self.assertTrue(verify_router_attributes(router, self.router_creator, ext_gateway=self.ext_net_name)) + + def test_create_delete_router(self): + """ + Test that clean() will not raise an exception if the router is deleted by another process. + """ + self.router_settings = RouterSettings(name=self.guid + '-pub-router', external_gateway=self.ext_net_name) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, self.router_settings) + created_router = self.router_creator.create() + self.assertIsNotNone(created_router) + retrieved_router = neutron_utils.get_router_by_name(self.neutron, self.router_settings.name) + self.assertIsNotNone(retrieved_router) + + neutron_utils.delete_router(self.neutron, created_router) + + retrieved_router = neutron_utils.get_router_by_name(self.neutron, self.router_settings.name) + self.assertIsNone(retrieved_router) + + # Should not raise an exception + self.router_creator.clean() + + def test_create_router_admin_state_false(self): + """ + Test creation of a basic router with admin state down. + """ + router_settings = RouterSettings(name=self.guid + '-pub-router', admin_state_up=False) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + self.assertIsNotNone(router) + + self.assertTrue(verify_router_attributes(router, self.router_creator, admin_state=False)) + + def test_create_router_admin_state_True(self): + """ + Test creation of a basic router with admin state Up. + """ + router_settings = RouterSettings(name=self.guid + '-pub-router', admin_state_up=True) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + self.assertIsNotNone(router) + + self.assertTrue(verify_router_attributes(router, self.router_creator, admin_state=True)) + + def test_create_router_private_network(self): + """ + Test creation of a router connected with two private networks and no external gateway + """ + network_settings1 = NetworkSettings(name=self.guid + '-pub-net1', + subnet_settings=[ + create_network.SubnetSettings(cidr=cidr1, + name=self.guid + '-pub-subnet1', + gateway_ip=static_gateway_ip1)]) + network_settings2 = NetworkSettings(name=self.guid + '-pub-net2', + subnet_settings=[ + create_network.SubnetSettings(cidr=cidr2, + name=self.guid + '-pub-subnet2', + gateway_ip=static_gateway_ip2)]) + + self.network_creator1 = OpenStackNetwork(self.os_creds, network_settings1) + self.network_creator2 = OpenStackNetwork(self.os_creds, network_settings2) + + self.network_creator1.create() + self.network_creator2.create() + + port_settings = [create_network.PortSettings(name=self.guid + '-port1', ip_addrs=[ + {'subnet_name': network_settings1.subnet_settings[0].name, 'ip': static_gateway_ip1}], + network_name=network_settings1.name) + , create_network.PortSettings(name=self.guid + '-port2', ip_addrs=[ + {'subnet_name': network_settings2.subnet_settings[0].name, 'ip': static_gateway_ip2}], + network_name=network_settings2.name)] + + router_settings = RouterSettings(name=self.guid + '-pub-router', port_settings=port_settings) + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + + self.assertTrue(verify_router_attributes(router, self.router_creator)) + + def test_create_router_external_network(self): + """ + Test creation of a router connected to an external network and a private network. + """ + network_settings = NetworkSettings(name=self.guid + '-pub-net1', + subnet_settings=[ + create_network.SubnetSettings(cidr=cidr1, + name=self.guid + '-pub-subnet1', + gateway_ip=static_gateway_ip1)]) + self.network_creator1 = OpenStackNetwork(self.os_creds, network_settings) + self.network_creator1.create() + + port_settings = [create_network.PortSettings(name=self.guid + '-port1', ip_addrs=[ + {'subnet_name': network_settings.subnet_settings[0].name, 'ip': static_gateway_ip1}], + network_name=network_settings.name)] + + router_settings = RouterSettings(name=self.guid + '-pub-router', + external_gateway=self.ext_net_name, port_settings=port_settings) + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + + self.assertTrue(verify_router_attributes(router, self.router_creator, ext_gateway=self.ext_net_name)) + + +class CreateRouterNegativeTests(OSIntegrationTestCase): + """ + Class for testing routers with various negative scenarios expected to fail. + """ + + def setUp(self): + """ + Initializes objects used for router testing + """ + super(self.__class__, self).__start__() + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.router_creator = None + + def tearDown(self): + """ + Cleans the remote OpenStack objects used for router testing + """ + if self.router_creator: + self.router_creator.clean() + + super(self.__class__, self).__clean__() + + def test_create_router_noname(self): + """ + Test creating a router without a name. + """ + with self.assertRaises(Exception): + router_settings = RouterSettings(name=None, external_gateway=self.ext_net_name) + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + def test_create_router_invalid_gateway_name(self): + """ + Test creating a router without a valid network gateway name. + """ + with self.assertRaises(Exception): + router_settings = RouterSettings(name=self.guid + '-pub-router', external_gateway="Invalid_name") + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + +def verify_router_attributes(router_operational, router_creator, admin_state=True, ext_gateway=None): + """ + Helper function to validate the attributes of router created with the one operational + :param router_operational: Operational Router object returned from neutron utils + :param router_creator: router_creator object returned from creating a router in the router test functions + :param admin_state: True if router is expected to be Up, else False + :param snat: True is enable_snat is True, else False + :param ext_gateway: None if router is not connected to external gateway + :return: + """ + + router = router_creator.get_router() + + if not router_operational: + return False + elif not router_creator: + return False + elif not (router_operational['router']['name'] == router_creator.router_settings.name): + return False + elif not (router_operational['router']['id'] == router['router']['id']): + return False + elif not (router_operational['router']['status'] == router['router']['status']): + return False + elif not (router_operational['router']['tenant_id'] == router['router']['tenant_id']): + return False + elif not (admin_state == router_operational['router']['admin_state_up']): + return False + elif (ext_gateway is None) and (router_operational['router']['external_gateway_info'] is not None): + return False + elif ext_gateway is not None: + if router_operational['router']['external_gateway_info'] is None: + return False + return True diff --git a/snaps/openstack/tests/create_security_group_tests.py b/snaps/openstack/tests/create_security_group_tests.py new file mode 100644 index 0000000..079be0c --- /dev/null +++ b/snaps/openstack/tests/create_security_group_tests.py @@ -0,0 +1,355 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack import create_security_group +from snaps.openstack.create_security_group import SecurityGroupSettings, SecurityGroupRuleSettings, Direction, \ + Ethertype, Protocol +from snaps.openstack.tests import validation_utils +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase +from snaps.openstack.utils import neutron_utils + +__author__ = 'spisarski' + + +class SecurityGroupRuleSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the SecurityGroupRuleSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings(sec_grp_name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings(config={'sec_grp_name': 'foo'}) + + def test_name_and_direction(self): + settings = SecurityGroupRuleSettings(sec_grp_name='foo', direction=Direction.ingress) + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals(Direction.ingress, settings.direction) + + def test_config_name_and_direction(self): + settings = SecurityGroupRuleSettings(config={'sec_grp_name': 'foo', 'direction': 'ingress'}) + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals(Direction.ingress, settings.direction) + + def test_all(self): + settings = SecurityGroupRuleSettings( + sec_grp_name='foo', description='fubar', direction=Direction.egress, remote_group_id='rgi', + protocol=Protocol.icmp, ethertype=Ethertype.IPv6, port_range_min=1, port_range_max=2, + remote_ip_prefix='prfx') + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals('fubar', settings.description) + self.assertEquals(Direction.egress, settings.direction) + self.assertEquals('rgi', settings.remote_group_id) + self.assertEquals(Protocol.icmp, settings.protocol) + self.assertEquals(Ethertype.IPv6, settings.ethertype) + self.assertEquals(1, settings.port_range_min) + self.assertEquals(2, settings.port_range_max) + self.assertEquals('prfx', settings.remote_ip_prefix) + + def test_config_all(self): + settings = SecurityGroupRuleSettings( + config={'sec_grp_name': 'foo', + 'description': 'fubar', + 'direction': 'egress', + 'remote_group_id': 'rgi', + 'protocol': 'tcp', + 'ethertype': 'IPv6', + 'port_range_min': 1, + 'port_range_max': 2, + 'remote_ip_prefix': 'prfx'}) + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals('fubar', settings.description) + self.assertEquals(Direction.egress, settings.direction) + self.assertEquals('rgi', settings.remote_group_id) + self.assertEquals(Protocol.tcp, settings.protocol) + self.assertEquals(Ethertype.IPv6, settings.ethertype) + self.assertEquals(1, settings.port_range_min) + self.assertEquals(2, settings.port_range_max) + self.assertEquals('prfx', settings.remote_ip_prefix) + + +class SecurityGroupSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the SecurityGroupSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + SecurityGroupSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + SecurityGroupSettings(config=dict()) + + def test_name_only(self): + settings = SecurityGroupSettings(name='foo') + self.assertEquals('foo', settings.name) + + def test_config_with_name_only(self): + settings = SecurityGroupSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + + def test_invalid_rule(self): + rule_setting = SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.ingress) + with self.assertRaises(Exception): + SecurityGroupSettings(name='foo', rule_settings=[rule_setting]) + + def test_all(self): + rule_settings = list() + rule_settings.append(SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.egress)) + rule_settings.append(SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.ingress)) + settings = SecurityGroupSettings( + name='bar', description='fubar', project_name='foo', rule_settings=rule_settings) + + self.assertEquals('bar', settings.name) + self.assertEquals('fubar', settings.description) + self.assertEquals('foo', settings.project_name) + self.assertEquals(rule_settings[0], settings.rule_settings[0]) + self.assertEquals(rule_settings[1], settings.rule_settings[1]) + + def test_config_all(self): + settings = SecurityGroupSettings( + config={'name': 'bar', + 'description': 'fubar', + 'project_name': 'foo', + 'rules': [{'sec_grp_name': 'bar', 'direction': 'ingress'}]}) + + self.assertEquals('bar', settings.name) + self.assertEquals('fubar', settings.description) + self.assertEquals('foo', settings.project_name) + self.assertEquals(1, len(settings.rule_settings)) + self.assertEquals('bar', settings.rule_settings[0].sec_grp_name) + self.assertEquals(Direction.ingress, settings.rule_settings[0].direction) + + +class CreateSecurityGroupTests(OSIntegrationTestCase): + """ + Test for the CreateSecurityGroup class defined in create_security_group.py + """ + + def setUp(self): + """ + Instantiates the CreateSecurityGroup object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.sec_grp_name = guid + 'name' + self.neutron = neutron_utils.neutron_client(self.os_creds) + + # Initialize for cleanup + self.sec_grp_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.sec_grp_creator: + self.sec_grp_creator.clean() + + super(self.__class__, self).__clean__() + + def test_create_group_without_rules(self): + """ + Tests the creation of an OpenStack Security Group without custom rules. + """ + # Create Image + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group') + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + self.assertIsNotNone(sec_grp) + + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + def test_create_delete_group(self): + """ + Tests the creation of an OpenStack Security Group without custom rules. + """ + # Create Image + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group') + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + created_sec_grp = self.sec_grp_creator.create() + self.assertIsNotNone(created_sec_grp) + + neutron_utils.delete_security_group(self.neutron, created_sec_grp) + self.assertIsNone(neutron_utils.get_security_group(self.neutron, self.sec_grp_creator.sec_grp_settings.name)) + + self.sec_grp_creator.clean() + + def test_create_group_with_one_simple_rule(self): + """ + Tests the creation of an OpenStack Security Group with one simple custom rule. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + def test_create_group_with_several_rules(self): + """ + Tests the creation of an OpenStack Security Group with one simple custom rule. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv6)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv4, + port_range_min=10, + port_range_max=20)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + def test_add_rule(self): + """ + Tests the creation of an OpenStack Security Group with one simple custom rule then adds one after creation. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + self.sec_grp_creator.add_rule(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_creator.sec_grp_settings.name, + direction=Direction.egress, protocol=Protocol.icmp)) + rules2 = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group()) + self.assertEquals(len(rules) + 1, len(rules2)) + + def test_remove_rule_by_id(self): + """ + Tests the creation of an OpenStack Security Group with two simple custom rules then removes one by the rule ID. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv6)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv4, + port_range_min=10, + port_range_max=20)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + self.sec_grp_creator.remove_rule(rule_id=rules[0]['security_group_rule']['id']) + rules_after_del = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(rules) - 1, len(rules_after_del)) + + def test_remove_rule_by_setting(self): + """ + Tests the creation of an OpenStack Security Group with two simple custom rules then removes one by the rule + setting object + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv6)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv4, + port_range_min=10, + port_range_max=20)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + self.sec_grp_creator.remove_rule(rule_setting=sec_grp_rule_settings[0]) + rules_after_del = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(rules) - 1, len(rules_after_del)) + +# TODO - Add more tests with different rules. Rule creation parameters can be somewhat complex diff --git a/snaps/openstack/tests/create_user_tests.py b/snaps/openstack/tests/create_user_tests.py new file mode 100644 index 0000000..1f7a163 --- /dev/null +++ b/snaps/openstack/tests/create_user_tests.py @@ -0,0 +1,155 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest +from snaps.openstack.create_user import OpenStackUser, UserSettings +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + + +class UserSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the UserSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + UserSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + UserSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + UserSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + UserSettings(config={'name': 'foo'}) + + def test_name_pass_enabled_str(self): + with self.assertRaises(Exception): + UserSettings(name='foo', password='bar', enabled='true') + + def test_config_with_name_pass_enabled_str(self): + with self.assertRaises(Exception): + UserSettings(config={'name': 'foo', 'password': 'bar', 'enabled': 'true'}) + + def test_name_pass_only(self): + settings = UserSettings(name='foo', password='bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.email) + self.assertTrue(settings.enabled) + + def test_config_with_name_pass_only(self): + settings = UserSettings(config={'name': 'foo', 'password': 'bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.email) + self.assertTrue(settings.enabled) + + def test_all(self): + settings = UserSettings(name='foo', password='bar', project_name='proj-foo', email='foo@bar.com', enabled=False) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertEquals('proj-foo', settings.project_name) + self.assertEquals('foo@bar.com', settings.email) + self.assertFalse(settings.enabled) + + def test_config_all(self): + settings = UserSettings(config={'name': 'foo', 'password': 'bar', 'project_name': 'proj-foo', + 'email': 'foo@bar.com', 'enabled': False}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertEquals('proj-foo', settings.project_name) + self.assertEquals('foo@bar.com', settings.email) + self.assertFalse(settings.enabled) + + +class CreateUserSuccessTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = str(uuid.uuid4())[:-19] + guid = self.__class__.__name__ + '-' + guid + self.user_settings = UserSettings(name=guid + '-name', password=guid + '-password') + + self.keystone = keystone_utils.keystone_client(self.os_creds) + + # Initialize for cleanup + self.user_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.user_creator: + self.user_creator.clean() + + def test_create_user(self): + """ + Tests the creation of an OpenStack user. + """ + self.user_creator = OpenStackUser(self.os_creds, self.user_settings) + created_user = self.user_creator.create() + self.assertIsNotNone(created_user) + + retrieved_user = keystone_utils.get_user(self.keystone, self.user_settings.name) + self.assertIsNotNone(retrieved_user) + self.assertEquals(created_user, retrieved_user) + + def test_create_user_2x(self): + """ + Tests the creation of an OpenStack user twice to ensure it only creates one. + """ + self.user_creator = OpenStackUser(self.os_creds, self.user_settings) + created_user = self.user_creator.create() + self.assertIsNotNone(created_user) + + retrieved_user = keystone_utils.get_user(self.keystone, self.user_settings.name) + self.assertIsNotNone(retrieved_user) + self.assertEquals(created_user, retrieved_user) + + # Create user for the second time to ensure it is the same + user2 = OpenStackUser(self.os_creds, self.user_settings).create() + self.assertEquals(retrieved_user, user2) + + def test_create_delete_user(self): + """ + Tests the creation of an OpenStack user then delete. + """ + # Create Image + self.user_creator = OpenStackUser(self.os_creds, self.user_settings) + created_user = self.user_creator.create() + self.assertIsNotNone(created_user) + + keystone_utils.delete_user(self.keystone, created_user) + + # Delete user + self.user_creator.clean() + self.assertIsNone(self.user_creator.get_user()) + diff --git a/snaps/openstack/tests/openstack_tests.py b/snaps/openstack/tests/openstack_tests.py new file mode 100644 index 0000000..dab2ea2 --- /dev/null +++ b/snaps/openstack/tests/openstack_tests.py @@ -0,0 +1,144 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from snaps import file_utils +from snaps.openstack.create_network import NetworkSettings, SubnetSettings +from snaps.openstack.create_router import RouterSettings +from snaps.openstack.os_credentials import OSCreds, ProxySettings +from snaps.openstack.create_image import ImageSettings +import logging + +__author__ = 'spisarski' + + +logger = logging.getLogger('openstack_tests') + + +def get_credentials(os_env_file=None, proxy_settings_str=None, ssh_proxy_cmd=None, dev_os_env_file=None): + """ + Returns the OpenStack credentials object. It first attempts to retrieve them from a standard OpenStack source file. + If that file is None, it will attempt to retrieve them with a YAML file. + it will retrieve them from a + :param os_env_file: the OpenStack source file + :param proxy_settings_str: proxy settings string <host>:<port> (optional) + :param ssh_proxy_cmd: the SSH proxy command for your environment (optional) + :param dev_os_env_file: the YAML file to retrieve both the OS credentials and proxy settings + :return: the SNAPS credentials object + """ + if os_env_file: + logger.debug('Reading RC file - ' + os_env_file) + config = file_utils.read_os_env_file(os_env_file) + proj_name = config.get('OS_PROJECT_NAME') + if not proj_name: + proj_name = config.get('OS_TENANT_NAME') + + proj_domain_id = 'default' + user_domain_id = 'default' + + if config.get('OS_PROJECT_DOMAIN_ID'): + proj_domain_id = config['OS_PROJECT_DOMAIN_ID'] + if config.get('OS_USER_DOMAIN_ID'): + user_domain_id = config['OS_USER_DOMAIN_ID'] + if config.get('OS_IDENTITY_API_VERSION'): + version = int(config['OS_IDENTITY_API_VERSION']) + else: + version = 2 + + proxy_settings = None + if proxy_settings_str: + tokens = re.split(':', proxy_settings_str) + proxy_settings = ProxySettings(tokens[0], tokens[1], ssh_proxy_cmd) + + os_creds = OSCreds(username=config['OS_USERNAME'], + password=config['OS_PASSWORD'], + auth_url=config['OS_AUTH_URL'], + project_name=proj_name, + identity_api_version=version, + user_domain_id=user_domain_id, + project_domain_id=proj_domain_id, + proxy_settings=proxy_settings) + else: + logger.info('Reading development os_env file - ' + dev_os_env_file) + config = file_utils.read_yaml(dev_os_env_file) + identity_api_version = config.get('identity_api_version') + if not identity_api_version: + identity_api_version = 2 + + proxy_settings = None + proxy_str = config.get('http_proxy') + if proxy_str: + tokens = re.split(':', proxy_str) + proxy_settings = ProxySettings(tokens[0], tokens[1], config.get('ssh_proxy_cmd')) + + os_creds = OSCreds(username=config['username'], password=config['password'], + auth_url=config['os_auth_url'], project_name=config['project_name'], + identity_api_version=identity_api_version, + proxy_settings=proxy_settings) + + logger.info('OS Credentials = ' + str(os_creds)) + return os_creds + + +def cirros_url_image(name): + return ImageSettings(name=name, image_user='cirros', img_format='qcow2', + url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img') + + +def file_image_test_settings(name, file_path): + return ImageSettings(name=name, image_user='cirros', img_format='qcow2', + image_file=file_path) + + +def centos_url_image(name): + return ImageSettings(name=name, image_user='centos', img_format='qcow2', + url='http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2', + nic_config_pb_loc='./provisioning/ansible/centos-network-setup/playbooks/configure_host.yml') + + +def ubuntu_url_image(name): + return ImageSettings( + name=name, image_user='ubuntu', img_format='qcow2', + url='http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img', + nic_config_pb_loc='./provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml') + + +def get_priv_net_config(net_name, subnet_name, router_name=None, cidr='10.55.0.0/24', external_net=None): + return OSNetworkConfig(net_name, subnet_name, cidr, router_name, external_gateway=external_net) + + +def get_pub_net_config(net_name, subnet_name=None, router_name=None, cidr='10.55.1.0/24', external_net=None): + return OSNetworkConfig(net_name, subnet_name, cidr, router_name, external_gateway=external_net) + + +class OSNetworkConfig: + """ + Represents the settings required for the creation of a network in OpenStack + """ + + def __init__(self, net_name, subnet_name=None, subnet_cidr=None, router_name=None, external_gateway=None): + + if subnet_name and subnet_cidr: + self.network_settings = NetworkSettings( + name=net_name, subnet_settings=[SubnetSettings(cidr=subnet_cidr, name=subnet_name)]) + else: + self.network_settings = NetworkSettings(name=net_name) + + if router_name: + if subnet_name: + self.router_settings = RouterSettings(name=router_name, external_gateway=external_gateway, + internal_subnets=[subnet_name]) + else: + self.router_settings = RouterSettings(name=router_name, external_gateway=external_gateway) diff --git a/snaps/openstack/tests/os_source_file_test.py b/snaps/openstack/tests/os_source_file_test.py new file mode 100644 index 0000000..fa8d197 --- /dev/null +++ b/snaps/openstack/tests/os_source_file_test.py @@ -0,0 +1,131 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +import uuid + +from snaps import file_utils +import openstack_tests +import logging + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# To run these tests from an IDE, the CWD must be set to the python directory of this project +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +from snaps.openstack.create_project import ProjectSettings +from snaps.openstack.create_user import UserSettings +from snaps.openstack.utils import deploy_utils, keystone_utils + +dev_os_env_file = 'openstack/tests/conf/os_env.yaml' + + +class OSComponentTestCase(unittest.TestCase): + + """ + Super for test classes requiring a connection to OpenStack + """ + def __init__(self, method_name='runTest', os_env_file=None, ext_net_name=None, http_proxy_str=None, + ssh_proxy_cmd=None, log_level=logging.DEBUG): + super(OSComponentTestCase, self).__init__(method_name) + + logging.basicConfig(level=log_level) + + self.os_creds = openstack_tests.get_credentials(os_env_file=os_env_file, proxy_settings_str=http_proxy_str, + ssh_proxy_cmd=ssh_proxy_cmd, dev_os_env_file=dev_os_env_file) + self.ext_net_name = ext_net_name + + if not self.ext_net_name and file_utils.file_exists(dev_os_env_file): + test_conf = file_utils.read_yaml(dev_os_env_file) + self.ext_net_name = test_conf.get('ext_net') + + @staticmethod + def parameterize(testcase_klass, os_env_file, ext_net_name, http_proxy_str=None, ssh_proxy_cmd=None, + log_level=logging.DEBUG): + """ Create a suite containing all tests taken from the given + subclass, passing them the parameter 'param'. + """ + test_loader = unittest.TestLoader() + test_names = test_loader.getTestCaseNames(testcase_klass) + suite = unittest.TestSuite() + for name in test_names: + suite.addTest(testcase_klass(name, os_env_file, ext_net_name, http_proxy_str, ssh_proxy_cmd, log_level)) + return suite + + +class OSIntegrationTestCase(OSComponentTestCase): + + """ + Super for test classes requiring a connection to OpenStack + """ + def __init__(self, method_name='runTest', os_env_file=None, ext_net_name=None, http_proxy_str=None, + ssh_proxy_cmd=None, use_keystone=False, log_level=logging.DEBUG): + super(OSIntegrationTestCase, self).__init__(method_name=method_name, os_env_file=os_env_file, + ext_net_name=ext_net_name, http_proxy_str=http_proxy_str, + ssh_proxy_cmd=ssh_proxy_cmd, log_level=log_level) + self.use_keystone = use_keystone + self.keystone = None + + @staticmethod + def parameterize(testcase_klass, os_env_file, ext_net_name, http_proxy_str=None, ssh_proxy_cmd=None, + use_keystone=False, log_level=logging.DEBUG): + """ Create a suite containing all tests taken from the given + subclass, passing them the parameter 'param'. + """ + test_loader = unittest.TestLoader() + test_names = test_loader.getTestCaseNames(testcase_klass) + suite = unittest.TestSuite() + for name in test_names: + suite.addTest(testcase_klass(name, os_env_file, ext_net_name, http_proxy_str, ssh_proxy_cmd, use_keystone, + log_level)) + return suite + + """ + Super for test classes that should be run within their own project/tenant as they can run for quite some time + """ + def __start__(self): + """ + Creates a project and user to be leveraged by subclass test methods. If implementing class uses this method, + it must call __clean__() else you will be left with unwanted users and tenants + """ + self.project_creator = None + self.user_creator = None + self.admin_os_creds = self.os_creds + self.role = None + + if self.use_keystone: + self.keystone = keystone_utils.keystone_client(self.os_creds) + guid = self.__class__.__name__ + '-' + str(uuid.uuid4())[:-19] + project_name = guid + '-proj' + self.project_creator = deploy_utils.create_project(self.admin_os_creds, ProjectSettings(name=project_name)) + + self.user_creator = deploy_utils.create_user( + self.admin_os_creds, UserSettings(name=guid + '-user', password=guid, project_name=project_name)) + self.os_creds = self.user_creator.get_os_creds(self.project_creator.project_settings.name) + + # add user to project + self.project_creator.assoc_user(self.user_creator.get_user()) + + def __clean__(self): + """ + Cleans up test user and project. + Must be called at the end of child classes tearDown() if __start__() is called during setUp() else these + objects will persist after the test is run + """ + if self.role: + keystone_utils.delete_role(self.keystone, self.role) + + if self.project_creator: + self.project_creator.clean() + + if self.user_creator: + self.user_creator.clean() diff --git a/snaps/openstack/tests/validation_utils.py b/snaps/openstack/tests/validation_utils.py new file mode 100644 index 0000000..7c9bd7f --- /dev/null +++ b/snaps/openstack/tests/validation_utils.py @@ -0,0 +1,69 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from neutronclient.v2_0.client import _DictWithMeta + +__author__ = 'spisarski' + + +def objects_equivalent(obj1, obj2): + """ + Returns true if both objects are equivalent + :param obj1: + :param obj2: + :return: T/F + """ + if obj1 is None and obj2 is None: + return True + if type(obj1) is dict or type(obj1) is _DictWithMeta: + return dicts_equivalent(obj1, obj2) + elif type(obj1) is list: + return lists_equivalent(obj1, obj2) + else: + return obj1 == obj2 + + +def dicts_equivalent(dict1, dict2): + """ + Returns true when each key/value pair is equal + :param dict1: dict 1 + :param dict2: dict 2 + :return: T/F + """ + if (type(dict1) is dict or type(dict1) is _DictWithMeta) and (type(dict2) is dict or type(dict2) is _DictWithMeta): + for key, value1 in dict1.iteritems(): + if not objects_equivalent(value1, dict2.get(key)): + return False + return True + return False + + +def lists_equivalent(list1, list2): + """ + Returns true when an item in list1 is also contained in list2 + :param list1: list 1 + :param list2: list 2 + :return: T/F + """ + if len(list1) == len(list2) and type(list1) is list and type(list2) is list: + for item1 in list1: + has_equivalent = False + for item2 in list2: + has_equivalent = objects_equivalent(item1, item2) + if has_equivalent: + break + if not has_equivalent: + return False + return True + return False diff --git a/snaps/openstack/utils/__init__.py b/snaps/openstack/utils/__init__.py new file mode 100644 index 0000000..7f92908 --- /dev/null +++ b/snaps/openstack/utils/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski'
\ No newline at end of file diff --git a/snaps/openstack/utils/deploy_utils.py b/snaps/openstack/utils/deploy_utils.py new file mode 100644 index 0000000..ade8811 --- /dev/null +++ b/snaps/openstack/utils/deploy_utils.py @@ -0,0 +1,151 @@ +# +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This utility makes it easy to create OpenStack objects +import logging + +from snaps.openstack.create_project import OpenStackProject +from snaps.openstack.create_user import OpenStackUser +from snaps.openstack.create_image import OpenStackImage +from snaps.openstack.create_network import OpenStackNetwork +from snaps.openstack.create_router import OpenStackRouter +from snaps.openstack.create_keypairs import OpenStackKeypair +from snaps.openstack.create_instance import OpenStackVmInstance +from snaps.openstack.create_security_group import OpenStackSecurityGroup + +logger = logging.getLogger('deploy_utils') + + +def create_image(os_creds, image_settings, cleanup=False): + """ + Creates an image in OpenStack if necessary + :param os_creds: The OpenStack credentials object + :param image_settings: The image settings object + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the image creator object from which the image object can be accessed + """ + image_creator = OpenStackImage(os_creds, image_settings) + image_creator.create(cleanup) + return image_creator + + +def create_network(os_creds, network_settings, cleanup=False): + """ + Creates a network on which the CMTSs can attach + :param os_creds: The OpenStack credentials object + :param network_settings: The network settings object + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the network creator objects for each network from which network elements such as the + subnet, router, interface router, and network objects can be accessed. + """ + # Check for OS for network existence + # If exists return network instance data + # Else, create network and return instance data + + logger.info('Attempting to create network with name - ' + network_settings.name) + + network_creator = OpenStackNetwork(os_creds, network_settings) + network_creator.create(cleanup) + logger.info('Created network ') + return network_creator + + +def create_router(os_creds, router_settings, cleanup=False): + """ + Creates a network on which the CMTSs can attach + :param os_creds: The OpenStack credentials object + :param router_settings: The RouterSettings instance + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the network creator objects for each network from which network elements such as the + subnet, router, interface router, and network objects can be accessed. + """ + # Check for OS for network existence + # If exists return network instance data + # Else, create network and return instance data + logger.info('Attempting to create router with name - ' + router_settings.name) + router_creator = OpenStackRouter(os_creds, router_settings) + router_creator.create(cleanup) + logger.info('Created router ') + return router_creator + + +def create_keypair(os_creds, keypair_settings, cleanup=False): + """ + Creates a keypair that can be applied to an instance + :param os_creds: The OpenStack credentials object + :param keypair_settings: The KeypairSettings object + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the keypair creator object + """ + keypair_creator = OpenStackKeypair(os_creds, keypair_settings) + keypair_creator.create(cleanup) + return keypair_creator + + +def create_vm_instance(os_creds, instance_settings, image_settings, keypair_creator=None, cleanup=False): + """ + Creates a VM instance + :param os_creds: The OpenStack credentials + :param instance_settings: Instance of VmInstanceSettings + :param image_settings: The object containing image settings + :param keypair_creator: The object responsible for creating the keypair associated with this VM instance. (optional) + :param sg_names: The names of the security groups to apply to VM. (optional) + :param cleanup: Denotes whether or not this is being called for cleanup or not (default False) + :return: A reference to the VM instance object + """ + kp_settings = None + if keypair_creator: + kp_settings = keypair_creator.keypair_settings + vm_creator = OpenStackVmInstance(os_creds, instance_settings, image_settings, kp_settings) + vm_creator.create(cleanup=cleanup) + return vm_creator + + +def create_user(os_creds, user_settings): + """ + Creates an OpenStack user + :param os_creds: The OpenStack credentials + :param user_settings: The user configuration settings + :return: A reference to the user instance object + """ + user_creator = OpenStackUser(os_creds, user_settings) + user_creator.create() + return user_creator + + +def create_project(os_creds, project_settings): + """ + Creates an OpenStack user + :param os_creds: The OpenStack credentials + :param project_settings: The user project configuration settings + :return: A reference to the project instance object + """ + project_creator = OpenStackProject(os_creds, project_settings) + project_creator.create() + return project_creator + + +def create_security_group(os_creds, sec_grp_settings): + """ + Creates an OpenStack Security Group + :param os_creds: The OpenStack credentials + :param sec_grp_settings: The security group settings + :return: A reference to the project instance object + """ + sg_creator = OpenStackSecurityGroup(os_creds, sec_grp_settings) + sg_creator.create() + return sg_creator + diff --git a/snaps/openstack/utils/glance_utils.py b/snaps/openstack/utils/glance_utils.py new file mode 100644 index 0000000..6d90d3e --- /dev/null +++ b/snaps/openstack/utils/glance_utils.py @@ -0,0 +1,78 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from snaps import file_utils +from glanceclient.client import Client +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('glance_utils') + +""" +Utilities for basic neutron API calls +""" + + +def glance_client(os_creds): + """ + Creates and returns a glance client object + :return: the glance client + """ + return Client(version=os_creds.image_api_version, session=keystone_utils.keystone_session(os_creds)) + + +def get_image(nova, glance, image_name): + """ + Returns an OpenStack image object for a given name + :param nova: the Nova client + :param glance: the Glance client + :param image_name: the image name to lookup + :return: the image object or None + """ + try: + image_dict = nova.images.find(name=image_name) + if image_dict: + return glance.images.get(image_dict.id) + except: + pass + return None + + +def create_image(glance, image_settings): + """ + Creates and returns OpenStack image object with an external URL + :param glance: the glance client + :param image_settings: the image settings object + :return: the OpenStack image object + :raise Exception if using a file and it cannot be found + """ + if image_settings.url: + return glance.images.create(name=image_settings.name, disk_format=image_settings.format, + container_format="bare", location=image_settings.url) + elif image_settings.image_file: + image_file = file_utils.get_file(image_settings.image_file) + return glance.images.create(name=image_settings.name, disk_format=image_settings.format, + container_format="bare", data=image_file) + + +def delete_image(glance, image): + """ + Deletes an image from OpenStack + :param glance: the glance client + :param image: the image to delete + """ + glance.images.delete(image) diff --git a/snaps/openstack/utils/keystone_utils.py b/snaps/openstack/utils/keystone_utils.py new file mode 100644 index 0000000..8175b9a --- /dev/null +++ b/snaps/openstack/utils/keystone_utils.py @@ -0,0 +1,204 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import requests +from keystoneclient.client import Client +from keystoneauth1.identity import v3, v2 +from keystoneauth1 import session +import logging + + +logger = logging.getLogger('keystone_utils') + +V2_VERSION = 'v2.0' + + +def keystone_session(os_creds): + """ + Creates a keystone session used for authenticating OpenStack clients + :param os_creds: The connection credentials to the OpenStack API + :return: the client object + """ + logger.debug('Retrieving Keystone Session') + + if os_creds.identity_api_version == 3: + auth = v3.Password(auth_url=os_creds.auth_url, username=os_creds.username, password=os_creds.password, + project_name=os_creds.project_name, user_domain_id=os_creds.user_domain_id, + project_domain_id=os_creds.project_domain_id) + else: + auth = v2.Password(auth_url=os_creds.auth_url, username=os_creds.username, password=os_creds.password, + tenant_name=os_creds.project_name) + + req_session = None + if os_creds.proxy_settings: + req_session = requests.Session() + req_session.proxies = {'http': os_creds.proxy_settings.host + ':' + os_creds.proxy_settings.port} + return session.Session(auth=auth, session=req_session) + + +def keystone_client(os_creds): + """ + Returns the keystone client + :param os_creds: the OpenStack credentials (OSCreds) object + :return: the client + """ + return Client(version=os_creds.identity_api_version, session=keystone_session(os_creds)) + + +def get_project(keystone=None, os_creds=None, project_name=None): + """ + Returns the first project object or None if not found + :param keystone: the Keystone client + :param os_creds: the OpenStack credentials used to obtain the Keystone client if the keystone parameter is None + :param project_name: the name to query + :return: the ID or None + """ + if not project_name: + return None + + if not keystone: + if os_creds: + keystone = keystone_client(os_creds) + else: + raise Exception('Cannot lookup project without the proper credentials') + + if keystone.version == V2_VERSION: + projects = keystone.tenants.list() + else: + projects = keystone.projects.list(**{'name': project_name}) + + for project in projects: + if project.name == project_name: + return project + + return None + + +def create_project(keystone, project_settings): + """ + Creates a project + :param keystone: the Keystone client + :param project_settings: the project configuration + :return: + """ + if keystone.version == V2_VERSION: + return keystone.tenants.create(project_settings.name, project_settings.description, project_settings.enabled) + + return keystone.projects.create(project_settings.name, project_settings.domain, + description=project_settings.description, + enabled=project_settings.enabled) + + +def delete_project(keystone, project): + """ + Deletes a project + :param keystone: the Keystone clien + :param project: the OpenStack project object + """ + if keystone.version == V2_VERSION: + keystone.tenants.delete(project) + else: + keystone.projects.delete(project) + + +def get_user(keystone, username, project_name=None): + """ + Returns a user for a given name and optionally project + :param keystone: the keystone client + :param username: the username to lookup + :param project_name: the associated project (optional) + :return: + """ + project = get_project(keystone=keystone, project_name=project_name) + + if project: + users = keystone.users.list(tenant_id=project.id) + else: + users = keystone.users.list() + + for user in users: + if user.name == username: + return user + + return None + + +def create_user(keystone, user_settings): + """ + Creates a user + :param keystone: the Keystone client + :param user_settings: the user configuration + :return: + """ + project = None + if user_settings.project_name: + project = get_project(keystone=keystone, project_name=user_settings.project_name) + + if keystone.version == V2_VERSION: + project_id = None + if project: + project_id = project.id + return keystone.users.create(name=user_settings.name, password=user_settings.password, + email=user_settings.email, tenant_id=project_id, enabled=user_settings.enabled) + else: + # TODO - need to support groups + return keystone.users.create(name=user_settings.name, password=user_settings.password, + email=user_settings.email, project=project, + # email=user_settings.email, project=project, group='default', + domain=user_settings.domain_name, + enabled=user_settings.enabled) + + +def delete_user(keystone, user): + """ + Deletes a user + :param keystone: the Keystone client + :param user: the OpenStack user object + """ + keystone.users.delete(user) + + +def create_role(keystone, name): + """ + Creates an OpenStack role + :param keystone: the keystone client + :param name: the role name + :return: + """ + return keystone.roles.create(name) + + +def delete_role(keystone, role): + """ + Deletes an OpenStack role + :param keystone: the keystone client + :param role: the role to delete + :return: + """ + keystone.roles.delete(role) + + +def assoc_user_to_project(keystone, role, user, project): + """ + Adds a user to a project + :param keystone: the Keystone client + :param role: the role used to join a project/user + :param user: the user to add to the project + :param project: the project to which to add a user + :return: + """ + if keystone.version == V2_VERSION: + keystone.roles.add_user_role(user, role, tenant=project) + else: + keystone.roles.grant(role, user=user, project=project) diff --git a/snaps/openstack/utils/neutron_utils.py b/snaps/openstack/utils/neutron_utils.py new file mode 100644 index 0000000..6c92d2e --- /dev/null +++ b/snaps/openstack/utils/neutron_utils.py @@ -0,0 +1,405 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from neutronclient.common.exceptions import NotFound +from neutronclient.neutron.client import Client +import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('neutron_utils') + +""" +Utilities for basic neutron API calls +""" + + +def neutron_client(os_creds): + """ + Instantiates and returns a client for communications with OpenStack's Neutron server + :param os_creds: the credentials for connecting to the OpenStack remote API + :return: the client object + """ + return Client(api_version=os_creds.network_api_version, session=keystone_utils.keystone_session(os_creds)) + + +def create_network(neutron, os_creds, network_settings): + """ + Creates a network for OpenStack + :param neutron: the client + :param os_creds: the OpenStack credentials + :param network_settings: A dictionary containing the network configuration and is responsible for creating the + network request JSON body + :return: the network object + """ + if neutron and network_settings: + logger.info('Creating network with name ' + network_settings.name) + json_body = network_settings.dict_for_neutron(os_creds) + return neutron.create_network(body=json_body) + else: + logger.error("Failed to create network") + raise Exception + + +def delete_network(neutron, network): + """ + Deletes a network for OpenStack + :param neutron: the client + :param network: the network object + """ + if neutron and network: + logger.info('Deleting network with name ' + network['network']['name']) + neutron.delete_network(network['network']['id']) + + +def get_network(neutron, network_name, project_id=None): + """ + Returns an object (dictionary) of the first network found with a given name and project_id (if included) + :param neutron: the client + :param network_name: the name of the network to retrieve + :param project_id: the id of the network's project + :return: + """ + net_filter = dict() + if network_name: + net_filter['name'] = network_name + if project_id: + net_filter['project_id'] = project_id + + networks = neutron.list_networks(**net_filter) + for network, netInsts in networks.iteritems(): + for inst in netInsts: + if inst.get('name') == network_name: + if project_id and inst.get('project_id') == project_id: + return {'network': inst} + else: + return {'network': inst} + return None + + +def get_network_by_id(neutron, network_id): + """ + Returns the network object (dictionary) with the given ID + :param neutron: the client + :param network_id: the id of the network to retrieve + :return: + """ + networks = neutron.list_networks(**{'id': network_id}) + for network, netInsts in networks.iteritems(): + for inst in netInsts: + if inst.get('id') == network_id: + return {'network': inst} + return None + + +def create_subnet(neutron, subnet_settings, os_creds, network=None): + """ + Creates a network subnet for OpenStack + :param neutron: the client + :param network: the network object + :param subnet_settings: A dictionary containing the subnet configuration and is responsible for creating the subnet + request JSON body + :param os_creds: the OpenStack credentials + :return: the subnet object + """ + if neutron and network and subnet_settings: + json_body = {'subnets': [subnet_settings.dict_for_neutron(os_creds, network=network)]} + logger.info('Creating subnet with name ' + subnet_settings.name) + subnets = neutron.create_subnet(body=json_body) + return {'subnet': subnets['subnets'][0]} + else: + logger.error("Failed to create subnet.") + raise Exception + + +def delete_subnet(neutron, subnet): + """ + Deletes a network subnet for OpenStack + :param neutron: the client + :param subnet: the subnet object + """ + if neutron and subnet: + logger.info('Deleting subnet with name ' + subnet['subnet']['name']) + neutron.delete_subnet(subnet['subnet']['id']) + + +def get_subnet_by_name(neutron, subnet_name): + """ + Returns the first subnet object (dictionary) found with a given name + :param neutron: the client + :param subnet_name: the name of the network to retrieve + :return: + """ + subnets = neutron.list_subnets(**{'name': subnet_name}) + for subnet, subnetInst in subnets.iteritems(): + for inst in subnetInst: + if inst.get('name') == subnet_name: + return {'subnet': inst} + return None + + +def create_router(neutron, os_creds, router_settings): + """ + Creates a router for OpenStack + :param neutron: the client + :param os_creds: the OpenStack credentials + :param router_settings: A dictionary containing the router configuration and is responsible for creating the subnet + request JSON body + :return: the router object + """ + if neutron: + json_body = router_settings.dict_for_neutron(neutron, os_creds) + logger.info('Creating router with name - ' + router_settings.name) + return neutron.create_router(json_body) + else: + logger.error("Failed to create router.") + raise Exception + + +def delete_router(neutron, router): + """ + Deletes a router for OpenStack + :param neutron: the client + :param router: the router object + """ + if neutron and router: + logger.info('Deleting router with name - ' + router['router']['name']) + neutron.delete_router(router=router['router']['id']) + return True + + +def get_router_by_name(neutron, router_name): + """ + Returns the first router object (dictionary) found with a given name + :param neutron: the client + :param router_name: the name of the network to retrieve + :return: + """ + routers = neutron.list_routers(**{'name': router_name}) + for router, routerInst in routers.iteritems(): + for inst in routerInst: + if inst.get('name') == router_name: + return {'router': inst} + return None + + +def add_interface_router(neutron, router, subnet=None, port=None): + """ + Adds an interface router for OpenStack for either a subnet or port. Exception will be raised if requesting for both. + :param neutron: the client + :param router: the router object + :param subnet: the subnet object + :param port: the port object + :return: the interface router object + """ + if subnet and port: + raise Exception('Cannot add interface to the router. Both subnet and port were sent in. Either or please.') + + if neutron and router and (router or subnet): + logger.info('Adding interface to router with name ' + router['router']['name']) + return neutron.add_interface_router(router=router['router']['id'], body=__create_port_json_body(subnet, port)) + else: + raise Exception("Unable to create interface router as neutron client, router or subnet were not created") + + +def remove_interface_router(neutron, router, subnet=None, port=None): + """ + Removes an interface router for OpenStack + :param neutron: the client + :param router: the router object + :param subnet: the subnet object (either subnet or port, not both) + :param port: the port object + """ + if router: + try: + logger.info('Removing router interface from router named ' + router['router']['name']) + neutron.remove_interface_router(router=router['router']['id'], body=__create_port_json_body(subnet, port)) + except NotFound as e: + logger.warn('Could not remove router interface. NotFound - ' + e.message) + pass + else: + logger.warn('Could not remove router interface, No router object') + + +def __create_port_json_body(subnet=None, port=None): + """ + Returns the dictionary required for creating and deleting router interfaces. Will only work on a subnet or port + object. Will throw and exception if parameters contain both or neither + :param subnet: the subnet object + :param port: the port object + :return: the dict + """ + if subnet and port: + raise Exception('Cannot create JSON body with both subnet and port') + if not subnet and not port: + raise Exception('Cannot create JSON body without subnet or port') + + if subnet: + return {"subnet_id": subnet['subnet']['id']} + else: + return {"port_id": port['port']['id']} + + +def create_port(neutron, os_creds, port_settings): + """ + Creates a port for OpenStack + :param neutron: the client + :param os_creds: the OpenStack credentials + :param port_settings: the settings object for port configuration + :return: the port object + """ + json_body = port_settings.dict_for_neutron(neutron, os_creds) + logger.info('Creating port for network with name - ' + port_settings.network_name) + return neutron.create_port(body=json_body) + + +def delete_port(neutron, port): + """ + Removes an OpenStack port + :param neutron: the client + :param port: the port object + :return: + """ + logger.info('Deleting port with name ' + port['port']['name']) + neutron.delete_port(port['port']['id']) + + +def get_port_by_name(neutron, port_name): + """ + Returns the first port object (dictionary) found with a given name + :param neutron: the client + :param port_name: the name of the port to retrieve + :return: + """ + ports = neutron.list_ports(**{'name': port_name}) + for port in ports['ports']: + if port['name'] == port_name: + return {'port': port} + return None + + +def create_security_group(neutron, keystone, sec_grp_settings): + """ + Creates a security group object in OpenStack + :param neutron: the Neutron client + :param keystone: the Keystone client + :param sec_grp_settings: the security group settings + :return: the security group object + """ + logger.info('Creating security group with name - ' + sec_grp_settings.name) + return neutron.create_security_group(sec_grp_settings.dict_for_neutron(keystone)) + + +def delete_security_group(neutron, sec_grp): + """ + Deletes a security group object from OpenStack + :param neutron: the client + :param sec_grp: the security group object to delete + """ + logger.info('Deleting security group with name - ' + sec_grp['security_group']['name']) + return neutron.delete_security_group(sec_grp['security_group']['id']) + + +def get_security_group(neutron, name): + """ + Returns the first security group object of the given name else None + :param neutron: the client + :param name: the name of security group object to retrieve + """ + logger.info('Retrieving security group with name - ' + name) + + groups = neutron.list_security_groups(**{'name': name}) + for group in groups['security_groups']: + if group['name'] == name: + return {'security_group': group} + return None + + +def get_security_group_by_id(neutron, sec_grp_id): + """ + Returns the first security group object of the given name else None + :param neutron: the client + :param sec_grp_id: the id of the security group to retrieve + """ + logger.info('Retrieving security group with ID - ' + sec_grp_id) + + groups = neutron.list_security_groups(**{'sec_grp_id': sec_grp_id}) + for group in groups['security_groups']: + return {'security_group': group} + return None + + +def create_security_group_rule(neutron, sec_grp_rule_settings): + """ + Creates a security group object in OpenStack + :param neutron: the client + :param sec_grp_rule_settings: the security group rule settings + :return: the security group object + """ + logger.info('Creating security group to security group - ' + sec_grp_rule_settings.sec_grp_name) + return neutron.create_security_group_rule(sec_grp_rule_settings.dict_for_neutron(neutron)) + + +def delete_security_group_rule(neutron, sec_grp_rule): + """ + Deletes a security group object from OpenStack + :param neutron: the client + :param sec_grp_rule: the security group rule object to delete + """ + logger.info('Deleting security group rule with ID - ' + sec_grp_rule['security_group_rule']['id']) + neutron.delete_security_group_rule(sec_grp_rule['security_group_rule']['id']) + + +def get_rules_by_security_group(neutron, sec_grp): + """ + Retrieves all of the rules for a given security group + :param neutron: the client + :param sec_grp: the security group object + """ + logger.info('Retrieving security group rules associate with the security group - ' + + sec_grp['security_group']['name']) + out = list() + rules = neutron.list_security_group_rules(**{'security_group_id': sec_grp['security_group']['id']}) + for rule in rules['security_group_rules']: + if rule['security_group_id'] == sec_grp['security_group']['id']: + out.append({'security_group_rule': rule}) + return out + + +def get_rule_by_id(neutron, sec_grp, rule_id): + """ + Deletes a security group object from OpenStack + :param neutron: the client + :param sec_grp: the security group object + :param rule_id: the rule's ID + """ + rules = neutron.list_security_group_rules(**{'security_group_id': sec_grp['security_group']['id']}) + for rule in rules['security_group_rules']: + if rule['id'] == rule_id: + return {'security_group_rule': rule} + return None + + +def get_external_networks(neutron): + """ + Returns a list of external OpenStack network object/dict for all external networks + :param neutron: the client + :return: a list of external networks (empty list if none configured) + """ + out = list() + for network in neutron.list_networks(**{'router:external': True})['networks']: + out.append({'network': network}) + return out diff --git a/snaps/openstack/utils/nova_utils.py b/snaps/openstack/utils/nova_utils.py new file mode 100644 index 0000000..9d0f70f --- /dev/null +++ b/snaps/openstack/utils/nova_utils.py @@ -0,0 +1,282 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import logging +import keystone_utils + +from novaclient.client import Client +from novaclient.exceptions import NotFound + +__author__ = 'spisarski' + +logger = logging.getLogger('nova_utils') + +""" +Utilities for basic OpenStack Nova API calls +""" + + +def nova_client(os_creds): + """ + Instantiates and returns a client for communications with OpenStack's Nova server + :param os_creds: The connection credentials to the OpenStack API + :return: the client object + """ + logger.debug('Retrieving Nova Client') + return Client(os_creds.compute_api_version, session=keystone_utils.keystone_session(os_creds)) + + +def get_servers_by_name(nova, name): + """ + Returns a list of servers with a given name + :param nova: the Nova client + :param name: the server name + :return: the list of servers + """ + return nova.servers.list(search_opts={'name': name}) + + +def get_latest_server_object(nova, server): + """ + Returns a server with a given id + :param nova: the Nova client + :param server: the old server object + :return: the list of servers or None if not found + """ + return nova.servers.get(server) + + +def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None): + """ + Saves the generated RSA generated keys to the filesystem + :param keys: the keys to save + :param pub_file_path: the path to the public keys + :param priv_file_path: the path to the private keys + :return: None + """ + if keys: + if pub_file_path: + pub_dir = os.path.dirname(pub_file_path) + if not os.path.isdir(pub_dir): + os.mkdir(pub_dir) + public_handle = open(pub_file_path, 'wb') + public_handle.write(keys.publickey().exportKey('OpenSSH')) + public_handle.close() + os.chmod(pub_file_path, 0o400) + logger.info("Saved public key to - " + pub_file_path) + if priv_file_path: + priv_dir = os.path.dirname(priv_file_path) + if not os.path.isdir(priv_dir): + os.mkdir(priv_dir) + private_handle = open(priv_file_path, 'wb') + private_handle.write(keys.exportKey()) + private_handle.close() + os.chmod(priv_file_path, 0o400) + logger.info("Saved private key to - " + priv_file_path) + + +def upload_keypair_file(nova, name, file_path): + """ + Uploads a public key from a file + :param nova: the Nova client + :param name: the keypair name + :param file_path: the path to the public key file + :return: the keypair object + """ + with open(os.path.expanduser(file_path)) as fpubkey: + logger.info('Saving keypair to - ' + file_path) + return upload_keypair(nova, name, fpubkey.read()) + + +def upload_keypair(nova, name, key): + """ + Uploads a public key from a file + :param nova: the Nova client + :param name: the keypair name + :param key: the public key object + :return: the keypair object + """ + logger.info('Creating keypair with name - ' + name) + return nova.keypairs.create(name=name, public_key=key) + + +def keypair_exists(nova, keypair_obj): + """ + Returns a copy of the keypair object if found + :param nova: the Nova client + :param keypair_obj: the keypair object + :return: the keypair object or None if not found + """ + try: + return nova.keypairs.get(keypair_obj) + except: + return None + + +def get_keypair_by_name(nova, name): + """ + Returns a list of all available keypairs + :param nova: the Nova client + :param name: the name of the keypair to lookup + :return: the keypair object or None if not found + """ + keypairs = nova.keypairs.list() + + for keypair in keypairs: + if keypair.name == name: + return keypair + + return None + + +def delete_keypair(nova, key): + """ + Deletes a keypair object from OpenStack + :param nova: the Nova client + :param key: the keypair object to delete + """ + logger.debug('Deleting keypair - ' + key.name) + nova.keypairs.delete(key) + + +def get_floating_ip_pools(nova): + """ + Returns all of the available floating IP pools + :param nova: the Nova client + :return: a list of pools + """ + return nova.floating_ip_pools.list() + + +def get_floating_ips(nova): + """ + Returns all of the floating IPs + :param nova: the Nova client + :return: a list of floating IPs + """ + return nova.floating_ips.list() + + +def create_floating_ip(nova, ext_net_name): + """ + Returns the floating IP object that was created with this call + :param nova: the Nova client + :param ext_net_name: the name of the external network on which to apply the floating IP address + :return: the floating IP object + """ + logger.info('Creating floating ip to external network - ' + ext_net_name) + return nova.floating_ips.create(ext_net_name) + + +def get_floating_ip(nova, floating_ip): + """ + Returns a floating IP object that should be identical to the floating_ip parameter + :param nova: the Nova client + :param floating_ip: the floating IP object to lookup + :return: hopefully the same floating IP object input + """ + logger.debug('Attempting to retrieve existing floating ip with IP - ' + floating_ip.ip) + return nova.floating_ips.get(floating_ip) + + +def delete_floating_ip(nova, floating_ip): + """ + Responsible for deleting a floating IP + :param nova: the Nova client + :param floating_ip: the floating IP object to delete + :return: + """ + logger.debug('Attempting to delete existing floating ip with IP - ' + floating_ip.ip) + return nova.floating_ips.delete(floating_ip) + + +def get_nova_availability_zones(nova): + """ + Returns the names of all nova compute servers + :param nova: the Nova client + :return: a list of compute server names + """ + out = list() + zones = nova.availability_zones.list() + for zone in zones: + if zone.zoneName == 'nova': + for key, host in zone.hosts.iteritems(): + out.append(zone.zoneName + ':' + key) + + return out + + +def delete_vm_instance(nova, vm_inst): + """ + Deletes a VM instance + :param nova: the nova client + :param vm_inst: the OpenStack instance object to delete + """ + nova.servers.delete(vm_inst) + + +def get_flavor_by_name(nova, name): + """ + Returns a flavor by name + :param nova: the Nova client + :param name: the flavor name to return + :return: the OpenStack flavor object or None if not exists + """ + try: + return nova.flavors.find(name=name) + except NotFound: + return None + + +def create_flavor(nova, flavor_settings): + """ + Creates and returns and OpenStack flavor object + :param nova: the Nova client + :param flavor_settings: the flavor settings + :return: the Flavor + """ + return nova.flavors.create(name=flavor_settings.name, flavorid=flavor_settings.flavor_id, ram=flavor_settings.ram, + vcpus=flavor_settings.vcpus, disk=flavor_settings.disk, + ephemeral=flavor_settings.ephemeral, swap=flavor_settings.swap, + rxtx_factor=flavor_settings.rxtx_factor, is_public=flavor_settings.is_public) + + +def delete_flavor(nova, flavor): + """ + Deletes a flavor + :param nova: the Nova client + :param flavor: the OpenStack flavor object + """ + nova.flavors.delete(flavor) + + +def add_security_group(nova, vm, security_group_name): + """ + Adds a security group to an existing VM + :param nova: the nova client + :param vm: the OpenStack server object (VM) to alter + :param security_group_name: the name of the security group to add + """ + nova.servers.add_security_group(vm.id, security_group_name) + + +def remove_security_group(nova, vm, security_group): + """ + Removes a security group from an existing VM + :param nova: the nova client + :param vm: the OpenStack server object (VM) to alter + :param security_group: the OpenStack security group object to add + """ + nova.servers.remove_security_group(vm.id, security_group) diff --git a/snaps/openstack/utils/tests/__init__.py b/snaps/openstack/utils/tests/__init__.py new file mode 100644 index 0000000..7f92908 --- /dev/null +++ b/snaps/openstack/utils/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski'
\ No newline at end of file diff --git a/snaps/openstack/utils/tests/glance_utils_tests.py b/snaps/openstack/utils/tests/glance_utils_tests.py new file mode 100644 index 0000000..d13908b --- /dev/null +++ b/snaps/openstack/utils/tests/glance_utils_tests.py @@ -0,0 +1,115 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import shutil +import uuid + +from snaps import file_utils +from snaps.openstack.tests import openstack_tests + +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests import validation_utils +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import glance_utils + +__author__ = 'spisarski' + + +class GlanceSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the neutron client can communicate with the cloud + """ + + def test_glance_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + glance = glance_utils.glance_client(self.os_creds) + + users = glance.images.list() + self.assertIsNotNone(users) + + def test_glance_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + with self.assertRaises(Exception): + neutron = glance_utils.glance_client(OSCreds('user', 'pass', 'url', 'project')) + neutron.list_networks() + + +class GlanceUtilsTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = uuid.uuid4() + self.image_name = self.__class__.__name__ + '-' + str(guid) + self.image = None + self.nova = nova_utils.nova_client(self.os_creds) + self.glance = glance_utils.glance_client(self.os_creds) + + self.tmp_dir = 'tmp/' + str(guid) + if not os.path.exists(self.tmp_dir): + os.makedirs(self.tmp_dir) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.image: + glance_utils.delete_image(self.glance, self.image) + + if os.path.exists(self.tmp_dir) and os.path.isdir(self.tmp_dir): + shutil.rmtree(self.tmp_dir) + + def test_create_image_minimal_url(self): + """ + Tests the glance_utils.create_image() function with a URL + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + + self.image = glance_utils.create_image(self.glance, os_image_settings) + self.assertIsNotNone(self.image) + + self.assertEqual(self.image_name, self.image.name) + + image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + self.assertIsNotNone(image) + + validation_utils.objects_equivalent(self.image, image) + + def test_create_image_minimal_file(self): + """ + Tests the glance_utils.create_image() function with a file + """ + url_image_settings = openstack_tests.cirros_url_image('foo') + image_file = file_utils.download(url_image_settings.url, self.tmp_dir) + file_image_settings = openstack_tests.file_image_test_settings(name=self.image_name, file_path=image_file.name) + + self.image = glance_utils.create_image(self.glance, file_image_settings) + self.assertIsNotNone(self.image) + self.assertEqual(self.image_name, self.image.name) + + image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name) + self.assertIsNotNone(image) + validation_utils.objects_equivalent(self.image, image) diff --git a/snaps/openstack/utils/tests/keystone_utils_tests.py b/snaps/openstack/utils/tests/keystone_utils_tests.py new file mode 100644 index 0000000..76a43ef --- /dev/null +++ b/snaps/openstack/utils/tests/keystone_utils_tests.py @@ -0,0 +1,100 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid + +from snaps.openstack.create_project import ProjectSettings +from snaps.openstack.create_user import UserSettings +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + + +class KeystoneSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the neutron client can communicate with the cloud + """ + + def test_keystone_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + keystone = keystone_utils.keystone_client(self.os_creds) + + users = keystone.users.list() + self.assertIsNotNone(users) + + def test_keystone_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + with self.assertRaises(Exception): + keystone = keystone_utils.keystone_client(OSCreds('user', 'pass', 'url', 'project')) + keystone.users.list() + + +class KeystoneUtilsTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = uuid.uuid4() + self.username = self.__class__.__name__ + '-' + str(guid) + self.user = None + + self.project_name = self.__class__.__name__ + '-' + str(guid) + self.project = None + self.keystone = keystone_utils.keystone_client(self.os_creds) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.project: + keystone_utils.delete_project(self.keystone, self.project) + + if self.user: + keystone_utils.delete_user(self.keystone, self.user) + + def test_create_user_minimal(self): + """ + Tests the keystone_utils.create_user() function + """ + user_settings = UserSettings(name=self.username, password='test123') + self.user = keystone_utils.create_user(self.keystone, user_settings) + self.assertEqual(self.username, self.user.name) + + user = keystone_utils.get_user(self.keystone, self.username) + self.assertIsNotNone(user) + self.assertEqual(self.user, user) + + def test_create_project_minimal(self): + """ + Tests the keyston_utils.create_project() funtion + """ + project_settings = ProjectSettings(name=self.project_name) + self.project = keystone_utils.create_project(self.keystone, project_settings) + self.assertEquals(self.project_name, self.project.name) + + project = keystone_utils.get_project(keystone=self.keystone, project_name=project_settings.name) + self.assertIsNotNone(project) + self.assertEquals(self.project_name, self.project.name) diff --git a/snaps/openstack/utils/tests/neutron_utils_tests.py b/snaps/openstack/utils/tests/neutron_utils_tests.py new file mode 100644 index 0000000..5f95fc9 --- /dev/null +++ b/snaps/openstack/utils/tests/neutron_utils_tests.py @@ -0,0 +1,651 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid + +from snaps.openstack.utils import keystone_utils +from snaps.openstack.create_security_group import SecurityGroupSettings, SecurityGroupRuleSettings, Direction +from snaps.openstack.tests import openstack_tests +from snaps.openstack.utils import neutron_utils +from snaps.openstack.create_network import NetworkSettings, SubnetSettings, PortSettings +from snaps.openstack import create_router +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.tests import validation_utils + +__author__ = 'spisarski' + +ip_1 = '10.55.1.100' +ip_2 = '10.55.1.200' + + +class NeutronSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the neutron client can communicate with the cloud + """ + + def test_neutron_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + neutron = neutron_utils.neutron_client(self.os_creds) + + networks = neutron.list_networks() + + found = False + networks = networks.get('networks') + for network in networks: + if network.get('name') == self.ext_net_name: + found = True + self.assertTrue(found) + + def test_neutron_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + with self.assertRaises(Exception): + neutron = neutron_utils.neutron_client( + OSCreds(username='user', password='pass', auth_url='url', project_name='project')) + neutron.list_networks() + + def test_retrieve_ext_network_name(self): + """ + Tests the neutron_utils.get_external_network_names to ensure the configured self.ext_net_name is contained + within the returned list + :return: + """ + neutron = neutron_utils.neutron_client(self.os_creds) + ext_networks = neutron_utils.get_external_networks(neutron) + found = False + for network in ext_networks: + if network['network']['name'] == self.ext_net_name: + found = True + break + self.assertTrue(found) + + +class NeutronUtilsNetworkTests(OSComponentTestCase): + """ + Test for creating networks via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.port_name = str(guid) + '-port' + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.network = None + self.net_config = openstack_tests.get_pub_net_config(net_name=guid + '-pub-net') + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.network: + neutron_utils.delete_network(self.neutron, self.network) + validate_network(self.neutron, self.network['network']['name'], False) + + def test_create_network(self): + """ + Tests the neutron_utils.create_neutron_net() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + def test_create_network_empty_name(self): + """ + Tests the neutron_utils.create_neutron_net() function with an empty network name + """ + with self.assertRaises(Exception): + self.network = neutron_utils.create_network(self.neutron, NetworkSettings(name='')) + + def test_create_network_null_name(self): + """ + Tests the neutron_utils.create_neutron_net() function when the network name is None + """ + with self.assertRaises(Exception): + self.network = neutron_utils.create_network(self.neutron, NetworkSettings()) + + +class NeutronUtilsSubnetTests(OSComponentTestCase): + """ + Test for creating networks with subnets via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.port_name = str(guid) + '-port' + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.network = None + self.subnet = None + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', external_net=self.ext_net_name) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.subnet: + neutron_utils.delete_subnet(self.neutron, self.subnet) + validate_subnet(self.neutron, self.subnet.get('name'), + self.net_config.network_settings.subnet_settings[0].cidr, False) + + if self.network: + neutron_utils.delete_network(self.neutron, self.network) + validate_network(self.neutron, self.network['network']['name'], False) + + def test_create_subnet(self): + """ + Tests the neutron_utils.create_neutron_net() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, network=self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + def test_create_subnet_null_name(self): + """ + Tests the neutron_utils.create_neutron_subnet() function for an Exception when the subnet name is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + with self.assertRaises(Exception): + SubnetSettings(cidr=self.net_config.subnet_cidr) + + def test_create_subnet_empty_name(self): + """ + Tests the neutron_utils.create_neutron_net() function with an empty name + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, network=self.network) + validate_subnet(self.neutron, '', self.net_config.network_settings.subnet_settings[0].cidr, True) + + def test_create_subnet_null_cidr(self): + """ + Tests the neutron_utils.create_neutron_subnet() function for an Exception when the subnet CIDR value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + with self.assertRaises(Exception): + sub_sets = SubnetSettings(cidr=None, name=self.net_config.subnet_name) + neutron_utils.create_subnet(self.neutron, sub_sets, self.os_creds, network=self.network) + + def test_create_subnet_empty_cidr(self): + """ + Tests the neutron_utils.create_neutron_subnet() function for an Exception when the subnet CIDR value is empty + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + with self.assertRaises(Exception): + sub_sets = SubnetSettings(cidr='', name=self.net_config.subnet_name) + neutron_utils.create_subnet(self.neutron, sub_sets, self.os_creds, network=self.network) + + +class NeutronUtilsRouterTests(OSComponentTestCase): + """ + Test for creating routers via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.port_name = str(guid) + '-port' + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.network = None + self.subnet = None + self.port = None + self.router = None + self.interface_router = None + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.interface_router: + neutron_utils.remove_interface_router(self.neutron, self.router, self.subnet) + + if self.router: + neutron_utils.delete_router(self.neutron, self.router) + validate_router(self.neutron, self.router.get('name'), False) + + if self.port: + neutron_utils.delete_port(self.neutron, self.port) + + if self.subnet: + neutron_utils.delete_subnet(self.neutron, self.subnet) + validate_subnet(self.neutron, self.subnet.get('name'), + self.net_config.network_settings.subnet_settings[0].cidr, False) + + if self.network: + neutron_utils.delete_network(self.neutron, self.network) + validate_network(self.neutron, self.network['network']['name'], False) + + def test_create_router_simple(self): + """ + Tests the neutron_utils.create_neutron_net() function when an external gateway is requested + """ + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + + def test_create_router_with_public_interface(self): + """ + Tests the neutron_utils.create_neutron_net() function when an external gateway is requested + """ + self.net_config = openstack_tests.OSNetworkConfig( + self.net_config.network_settings.name, + self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, self.net_config.router_settings.name, + self.ext_net_name) + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + # TODO - Add validation that the router gatway has been set + + def test_create_router_empty_name(self): + """ + Tests the neutron_utils.create_neutron_net() function + """ + with self.assertRaises(Exception): + this_router_settings = create_router.RouterSettings(name='') + self.router = neutron_utils.create_router(self.neutron, self.os_creds, this_router_settings) + + def test_create_router_null_name(self): + """ + Tests the neutron_utils.create_neutron_subnet() function when the subnet CIDR value is None + """ + with self.assertRaises(Exception): + this_router_settings = create_router.RouterSettings() + self.router = neutron_utils.create_router(self.neutron, self.os_creds, this_router_settings) + validate_router(self.neutron, None, True) + + def test_add_interface_router(self): + """ + Tests the neutron_utils.add_interface_router() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + + self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet) + validate_interface_router(self.interface_router, self.router, self.subnet) + + def test_add_interface_router_null_router(self): + """ + Tests the neutron_utils.add_interface_router() function for an Exception when the router value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet) + + def test_add_interface_router_null_subnet(self): + """ + Tests the neutron_utils.add_interface_router() function for an Exception when the subnet value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + + with self.assertRaises(Exception): + self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet) + + def test_create_port(self): + """ + Tests the neutron_utils.create_port() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + self.port = neutron_utils.create_port( + self.neutron, self.os_creds, PortSettings( + name=self.port_name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}], + network_name=self.net_config.network_settings.name)) + validate_port(self.neutron, self.port, self.port_name) + + def test_create_port_empty_name(self): + """ + Tests the neutron_utils.create_port() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + self.port = neutron_utils.create_port( + self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}])) + validate_port(self.neutron, self.port, self.port_name) + + def test_create_port_null_name(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the port name value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': ip_1}])) + + def test_create_port_null_network_object(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the network object is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + self.neutron, self.port_name, self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': ip_1}])) + + def test_create_port_null_ip(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the IP value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': None}])) + + def test_create_port_invalid_ip(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the IP value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': 'foo'}])) + + def test_create_port_invalid_ip_to_subnet(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the IP value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, + 'ip': '10.197.123.100'}])) + + +class NeutronUtilsSecurityGroupTests(OSComponentTestCase): + """ + Test for creating security groups via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.sec_grp_name = guid + 'name' + + self.security_group = None + self.security_group_rules = list() + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.keystone = keystone_utils.keystone_client(self.os_creds) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + for rule in self.security_group_rules: + neutron_utils.delete_security_group_rule(self.neutron, rule) + + if self.security_group: + neutron_utils.delete_security_group(self.neutron, self.security_group) + + def test_create_delete_simple_sec_grp(self): + """ + Tests the neutron_utils.create_security_group() function + """ + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name) + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + + self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name']) + + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNotNone(sec_grp_get) + self.assertTrue(validation_utils.objects_equivalent( + self.security_group['security_group'], sec_grp_get['security_group'])) + + neutron_utils.delete_security_group(self.neutron, self.security_group) + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNone(sec_grp_get) + self.security_group = None + + def test_create_sec_grp_no_name(self): + """ + Tests the SecurityGroupSettings constructor and neutron_utils.create_security_group() function to ensure + that attempting to create a security group without a name will raise an exception + """ + with self.assertRaises(Exception): + sec_grp_settings = SecurityGroupSettings() + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + + def test_create_sec_grp_no_rules(self): + """ + Tests the neutron_utils.create_security_group() function + """ + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group') + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + + self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name']) + self.assertTrue(sec_grp_settings.description, self.security_group['security_group']['description']) + + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNotNone(sec_grp_get) + self.assertTrue(validation_utils.objects_equivalent( + self.security_group['security_group'], sec_grp_get['security_group'])) + + def test_create_sec_grp_one_rule(self): + """ + Tests the neutron_utils.create_security_group() function + """ + + sec_grp_rule_settings = SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, direction=Direction.ingress) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=[sec_grp_rule_settings]) + + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + free_rules = neutron_utils.get_rules_by_security_group(self.neutron, self.security_group) + for free_rule in free_rules: + self.security_group_rules.append(free_rule) + + self.security_group_rules.append( + neutron_utils.create_security_group_rule(self.neutron, sec_grp_settings.rule_settings[0])) + + # Refresh object so it is populated with the newly added rule + self.security_group = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + + rules = neutron_utils.get_rules_by_security_group(self.neutron, self.security_group) + + self.assertTrue(validation_utils.objects_equivalent(self.security_group_rules, rules)) + + self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name']) + self.assertTrue(sec_grp_settings.description, self.security_group['security_group']['description']) + + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNotNone(sec_grp_get) + self.assertTrue(validation_utils.objects_equivalent( + self.security_group['security_group'], sec_grp_get['security_group'])) + + +""" +Validation routines +""" + + +def validate_network(neutron, name, exists): + """ + Returns true if a network for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a network for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param name: The expected network name + :param exists: Whether or not the network name should exist or not + :return: True/False + """ + network = neutron_utils.get_network(neutron, name) + if exists and network: + return True + if not exists and not network: + return True + return False + + +def validate_subnet(neutron, name, cidr, exists): + """ + Returns true if a subnet for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a subnet for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param name: The expected subnet name + :param cidr: The expected CIDR value + :param exists: Whether or not the network name should exist or not + :return: True/False + """ + subnet = neutron_utils.get_subnet_by_name(neutron, name) + if exists and subnet: + return subnet.get('cidr') == cidr + if not exists and not subnet: + return True + return False + + +def validate_router(neutron, name, exists): + """ + Returns true if a router for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a router for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param name: The expected router name + :param exists: Whether or not the network name should exist or not + :return: True/False + """ + router = neutron_utils.get_router_by_name(neutron, name) + if exists and router: + return True + return False + + +def validate_interface_router(interface_router, router, subnet): + """ + Returns true if the router ID & subnet ID have been properly included into the interface router object + :param interface_router: the object to validate + :param router: to validate against the interface_router + :param subnet: to validate against the interface_router + :return: True if both IDs match else False + """ + subnet_id = interface_router.get('subnet_id') + router_id = interface_router.get('port_id') + + return subnet.get('id') == subnet_id and router.get('id') == router_id + + +def validate_port(neutron, port_obj, this_port_name): + """ + Returns true if a port for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a port for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param port_obj: The port object to lookup + :param this_port_name: The expected router name + :return: True/False + """ + ports = neutron.list_ports() + for port, port_insts in ports.iteritems(): + for inst in port_insts: + if inst['id'] == port_obj['port']['id']: + return inst['name'] == this_port_name + return False diff --git a/snaps/openstack/utils/tests/nova_utils_tests.py b/snaps/openstack/utils/tests/nova_utils_tests.py new file mode 100644 index 0000000..f6c9156 --- /dev/null +++ b/snaps/openstack/utils/tests/nova_utils_tests.py @@ -0,0 +1,208 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os +import uuid + +from Crypto.PublicKey import RSA + +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.create_flavor import FlavorSettings + +__author__ = 'spisarski' + +logger = logging.getLogger('nova_utils_tests') + + +class NovaSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the nova client can communicate with the cloud + """ + + def test_nova_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + nova = nova_utils.nova_client(self.os_creds) + + # This should not throw an exception + nova.flavors.list() + + def test_nova_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + nova = nova_utils.nova_client( + OSCreds(username='user', password='pass', auth_url=self.os_creds.auth_url, + project_name=self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings)) + + # This should throw an exception + with self.assertRaises(Exception): + nova.flavors.list() + + +class NovaUtilsKeypairTests(OSComponentTestCase): + """ + Test basic nova keypair functionality + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.priv_key_file_path = 'tmp/' + guid + self.pub_key_file_path = self.priv_key_file_path + '.pub' + + self.nova = nova_utils.nova_client(self.os_creds) + self.keys = RSA.generate(1024) + self.public_key = self.keys.publickey().exportKey('OpenSSH') + self.keypair_name = guid + self.keypair = None + self.floating_ip = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.keypair: + try: + nova_utils.delete_keypair(self.nova, self.keypair) + except: + pass + + try: + os.remove(self.priv_key_file_path) + except: + pass + + try: + os.remove(self.pub_key_file_path) + except: + pass + + if self.floating_ip: + nova_utils.delete_floating_ip(self.nova, self.floating_ip) + + def test_create_keypair(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.keypair = nova_utils.upload_keypair(self.nova, self.keypair_name, self.public_key) + result = nova_utils.keypair_exists(self.nova, self.keypair) + self.assertEquals(self.keypair, result) + keypair = nova_utils.get_keypair_by_name(self.nova, self.keypair_name) + self.assertEquals(self.keypair, keypair) + + def test_create_delete_keypair(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.keypair = nova_utils.upload_keypair(self.nova, self.keypair_name, self.public_key) + result = nova_utils.keypair_exists(self.nova, self.keypair) + self.assertEquals(self.keypair, result) + nova_utils.delete_keypair(self.nova, self.keypair) + result2 = nova_utils.keypair_exists(self.nova, self.keypair) + self.assertIsNone(result2) + + def test_create_key_from_file(self): + """ + Tests that the generated RSA keys are properly saved to files + :return: + """ + nova_utils.save_keys_to_files(self.keys, self.pub_key_file_path, self.priv_key_file_path) + self.keypair = nova_utils.upload_keypair_file(self.nova, self.keypair_name, self.pub_key_file_path) + pub_key = open(os.path.expanduser(self.pub_key_file_path)).read() + self.assertEquals(self.keypair.public_key, pub_key) + + def test_floating_ips(self): + """ + Tests the creation of a floating IP + :return: + """ + ips = nova_utils.get_floating_ips(self.nova) + self.assertIsNotNone(ips) + + self.floating_ip = nova_utils.create_floating_ip(self.nova, self.ext_net_name) + returned = nova_utils.get_floating_ip(self.nova, self.floating_ip) + self.assertEquals(self.floating_ip, returned) + + +class NovaUtilsFlavorTests(OSComponentTestCase): + """ + Test basic nova flavor functionality + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.flavor_settings = FlavorSettings(name=guid + '-name', flavor_id=guid + '-id', ram=1, disk=1, vcpus=1, + ephemeral=1, swap=2, rxtx_factor=3.0, is_public=False) + self.nova = nova_utils.nova_client(self.os_creds) + self.flavor = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.flavor: + try: + nova_utils.delete_flavor(self.nova, self.flavor) + except: + pass + + def test_create_flavor(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.flavor = nova_utils.create_flavor(self.nova, self.flavor_settings) + self.validate_flavor() + + def test_create_delete_flavor(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.flavor = nova_utils.create_flavor(self.nova, self.flavor_settings) + self.validate_flavor() + nova_utils.delete_flavor(self.nova, self.flavor) + flavor = nova_utils.get_flavor_by_name(self.nova, self.flavor_settings.name) + self.assertIsNone(flavor) + + def validate_flavor(self): + """ + Validates the flavor_settings against the OpenStack flavor object + """ + self.assertIsNotNone(self.flavor) + self.assertEquals(self.flavor_settings.name, self.flavor.name) + self.assertEquals(self.flavor_settings.flavor_id, self.flavor.id) + self.assertEquals(self.flavor_settings.ram, self.flavor.ram) + self.assertEquals(self.flavor_settings.disk, self.flavor.disk) + self.assertEquals(self.flavor_settings.vcpus, self.flavor.vcpus) + self.assertEquals(self.flavor_settings.ephemeral, self.flavor.ephemeral) + + if self.flavor_settings.swap == 0: + self.assertEquals('', self.flavor.swap) + else: + self.assertEquals(self.flavor_settings.swap, self.flavor.swap) + + self.assertEquals(self.flavor_settings.rxtx_factor, self.flavor.rxtx_factor) + self.assertEquals(self.flavor_settings.is_public, self.flavor.is_public) diff --git a/snaps/playbook_runner.py b/snaps/playbook_runner.py new file mode 100644 index 0000000..3710309 --- /dev/null +++ b/snaps/playbook_runner.py @@ -0,0 +1,58 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging + +import re + +from snaps.openstack.os_credentials import ProxySettings +from snaps.provisioning import ansible_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('playbook_runner') + + +def main(parsed_args): + """ + Uses ansible_utils for applying Ansible Playbooks to machines with a private key + """ + logging.basicConfig(level=logging.DEBUG) + logger.info('Starting Playbook Runner') + + proxy_settings = None + if parsed_args.http_proxy: + tokens = re.split(':', parsed_args.http_proxy) + proxy_settings = ProxySettings(tokens[0], tokens[1], parsed_args.ssh_proxy_cmd) + + # Ensure can get an SSH client + ansible_utils.ssh_client(parsed_args.ip_addr, parsed_args.host_user, parsed_args.priv_key, proxy_settings) + + retval = ansible_utils.apply_playbook(parsed_args.playbook, [parsed_args.ip_addr], parsed_args.host_user, + parsed_args.priv_key, variables={'name': 'Foo'}, proxy_setting=proxy_settings) + exit(retval) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-a', '--ip-addr', dest='ip_addr', required=True, help='The Host IP Address') + parser.add_argument('-k', '--priv-key', dest='priv_key', required=True, help='The location of the private key file') + parser.add_argument('-u', '--host-user', dest='host_user', required=True, help='Host user account') + parser.add_argument('-b', '--playbook', dest='playbook', required=True, help='Playbook Location') + parser.add_argument('-p', '--http-proxy', dest='http_proxy', required=False, help='<host>:<port>') + parser.add_argument('-s', '--ssh-proxy-cmd', dest='ssh_proxy_cmd', required=False) + args = parser.parse_args() + + main(args) diff --git a/snaps/provisioning/__init__.py b/snaps/provisioning/__init__.py new file mode 100644 index 0000000..7f92908 --- /dev/null +++ b/snaps/provisioning/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski'
\ No newline at end of file diff --git a/snaps/provisioning/ansible/centos-network-setup/playbooks/configure_host.yml b/snaps/provisioning/ansible/centos-network-setup/playbooks/configure_host.yml new file mode 100644 index 0000000..8df03cb --- /dev/null +++ b/snaps/provisioning/ansible/centos-network-setup/playbooks/configure_host.yml @@ -0,0 +1,26 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- name: Configure NIC + hosts: all + become: yes + become_method: sudo + become_user: root + + tasks: + - name: Setup /etc/sysconfig/network-scripts/ifcfg-eth1 file + action: template owner=root group=root mode=644 src=../templates/ifcfg-interface dest=/etc/sysconfig/network-scripts/ifcfg-{{nic_name}} + - name : Restart Network + command: systemctl restart network
\ No newline at end of file diff --git a/snaps/provisioning/ansible/centos-network-setup/templates/ifcfg-interface b/snaps/provisioning/ansible/centos-network-setup/templates/ifcfg-interface new file mode 100644 index 0000000..47aa3fa --- /dev/null +++ b/snaps/provisioning/ansible/centos-network-setup/templates/ifcfg-interface @@ -0,0 +1,14 @@ +DEVICE={{ nic_name }} +NAME={{ nic_name }} +IPADDR={{ nic_ip }} + +DEFROUTE=no +NETMASK=255.255.255.0 +NM_CONTROLLED=no +IPV6INIT=yes +IPV6_AUTOCONF=yes +IPV6_DEFROUTE=yes +IPV6_PEERDNS=yes +IPV6_PEERROUTES=yes +IPV6_FAILURE_FATAL=no +ONBOOT=yes
\ No newline at end of file diff --git a/snaps/provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml b/snaps/provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml new file mode 100644 index 0000000..5d43f96 --- /dev/null +++ b/snaps/provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml @@ -0,0 +1,26 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- name: Configure NIC + hosts: all + become: yes + become_method: sudo + become_user: root + + tasks: + - name: Setup /etc/network/interfaces.d/{{nic_name}}.cfg file + action: template owner=root group=root mode=644 src=../templates/ethN.cfg dest=/etc/network/interfaces.d/{{nic_name}}.cfg + - name : Restart Network + command: service networking restart
\ No newline at end of file diff --git a/snaps/provisioning/ansible/ubuntu-network-setup/templates/ethN.cfg b/snaps/provisioning/ansible/ubuntu-network-setup/templates/ethN.cfg new file mode 100644 index 0000000..3fa7708 --- /dev/null +++ b/snaps/provisioning/ansible/ubuntu-network-setup/templates/ethN.cfg @@ -0,0 +1,2 @@ +auto {{ nic_name }} +iface {{ nic_name }} inet dhcp diff --git a/snaps/provisioning/ansible_utils.py b/snaps/provisioning/ansible_utils.py new file mode 100644 index 0000000..36f1efc --- /dev/null +++ b/snaps/provisioning/ansible_utils.py @@ -0,0 +1,114 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from collections import namedtuple + +import os +import paramiko + +from ansible.parsing.dataloader import DataLoader +from ansible.vars import VariableManager +from ansible.inventory import Inventory +from ansible.executor.playbook_executor import PlaybookExecutor + +__author__ = 'spisarski' + +logger = logging.getLogger('ansible_utils') + + +def apply_playbook(playbook_path, hosts_inv, host_user, ssh_priv_key_file_path, variables=None, proxy_setting=None): + """ + Executes an Ansible playbook to the given host + :param playbook_path: the (relative) path to the Ansible playbook + :param hosts_inv: a list of hostnames/ip addresses to which to apply the Ansible playbook + :param host_user: A user for the host instances (must be a password-less sudo user if playbook has "sudo: yes" + :param ssh_priv_key_file_path: the file location of the ssh key + :param variables: a dictionary containing any substitution variables needed by the Jinga 2 templates + :param proxy_setting: instance of os_credentials.ProxySettings class + :return: the results + """ + if not os.path.isfile(playbook_path): + raise Exception('Requested playbook not found - ' + playbook_path) + if not os.path.isfile(ssh_priv_key_file_path): + raise Exception('Requested private SSH key not found - ' + ssh_priv_key_file_path) + + import ansible.constants + ansible.constants.HOST_KEY_CHECKING = False + + variable_manager = VariableManager() + if variables: + variable_manager.extra_vars = variables + + loader = DataLoader() + inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=hosts_inv) + variable_manager.set_inventory(inventory) + + ssh_extra_args = None + if proxy_setting and proxy_setting.ssh_proxy_cmd: + ssh_extra_args = '-o ProxyCommand=\'' + proxy_setting.ssh_proxy_cmd + '\'' + + options = namedtuple('Options', ['listtags', 'listtasks', 'listhosts', 'syntax', 'connection', 'module_path', + 'forks', 'remote_user', 'private_key_file', 'ssh_common_args', 'ssh_extra_args', + 'become', 'become_method', 'become_user', 'verbosity', 'check']) + + ansible_opts = options(listtags=False, listtasks=False, listhosts=False, syntax=False, connection='ssh', + module_path=None, forks=100, remote_user=host_user, private_key_file=ssh_priv_key_file_path, + ssh_common_args=None, ssh_extra_args=ssh_extra_args, become=None, become_method=None, + become_user=None, verbosity=11111, check=False) + + logger.debug('Setting up Ansible Playbook Executor for playbook - ' + playbook_path) + executor = PlaybookExecutor( + playbooks=[playbook_path], + inventory=inventory, + variable_manager=variable_manager, + loader=loader, + options=ansible_opts, + passwords=None) + + logger.debug('Executing Ansible Playbook - ' + playbook_path) + retval = executor.run() + + if retval != 0: + logger.error('Playbook application failed [' + playbook_path + '] with return value of - ' + str(retval)) + raise Exception('Playbook not applied - ' + playbook_path) + + return retval + + +def ssh_client(ip, user, private_key_filepath, proxy_settings=None): + """ + Retrieves and attemts an SSH connection + :param ip: the IP of the host to connect + :param user: the user with which to connect + :param private_key_filepath: the path to the private key file + :param proxy_settings: instance of os_credentials.ProxySettings class (optional) + :return: the SSH client if can connect else false + """ + logger.debug('Retrieving SSH client') + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy()) + + try: + proxy_cmd = None + if proxy_settings and proxy_settings.ssh_proxy_cmd: + proxy_cmd_str = str(proxy_settings.ssh_proxy_cmd.replace('%h', ip)) + proxy_cmd_str = proxy_cmd_str.replace("%p", '22') + proxy_cmd = paramiko.ProxyCommand(proxy_cmd_str) + + ssh.connect(ip, username=user, key_filename=private_key_filepath, sock=proxy_cmd) + return ssh + except Exception as e: + logger.warn('Unable to connect via SSH with message - ' + e.message) diff --git a/snaps/provisioning/tests/__init__.py b/snaps/provisioning/tests/__init__.py new file mode 100644 index 0000000..e3e876e --- /dev/null +++ b/snaps/provisioning/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' diff --git a/snaps/provisioning/tests/ansible_utils_tests.py b/snaps/provisioning/tests/ansible_utils_tests.py new file mode 100644 index 0000000..dc108e0 --- /dev/null +++ b/snaps/provisioning/tests/ansible_utils_tests.py @@ -0,0 +1,217 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import uuid + +from snaps.openstack import create_instance +from snaps.openstack import create_keypairs +from snaps.openstack import create_network +from snaps.openstack import create_router +from snaps.openstack import create_image +from snaps.openstack import create_flavor +from scp import SCPClient + +from snaps.provisioning import ansible_utils +from snaps.openstack.tests import openstack_tests +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase + +VM_BOOT_TIMEOUT = 600 + +ip_1 = '10.0.1.100' +ip_2 = '10.0.1.200' + + +class AnsibleProvisioningTests(OSIntegrationTestCase): + """ + Test for the CreateInstance class with two NIC/Ports, eth0 with floating IP and eth1 w/o + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.keypair_priv_filepath = 'tmp/' + guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = guid + '-kp' + self.vm_inst_name = guid + '-inst' + self.test_file_local_path = 'tmp/' + guid + '-hello.txt' + self.port_1_name = guid + '-port-1' + self.port_2_name = guid + '-port-2' + self.floating_ip_name = guid + 'fip1' + + # Setup members to cleanup just in case they don't get created + self.inst_creator = None + self.keypair_creator = None + self.flavor_creator = None + self.router_creator = None + self.network_creator = None + self.image_creator = None + + try: + # Create Image + os_image_settings = openstack_tests.ubuntu_url_image(name=guid + '-' + '-image') + self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings) + self.image_creator.create() + + # First network is public + self.pub_net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + + self.network_creator = create_network.OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings) + self.network_creator.create() + + # Create routers + self.router_creator = create_router.OpenStackRouter(self.os_creds, self.pub_net_config.router_settings) + self.router_creator.create() + + # Create Flavor + self.flavor_creator = create_flavor.OpenStackFlavor( + self.admin_os_creds, + create_flavor.FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + + # Create Key/Pair + self.keypair_creator = create_keypairs.OpenStackKeypair( + self.os_creds, create_keypairs.KeypairSettings( + name=self.keypair_name, public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + + # Create instance + ports_settings = list() + ports_settings.append( + create_network.PortSettings(name=self.port_1_name, + network_name=self.pub_net_config.network_settings.name)) + + instance_settings = create_instance.VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=ports_settings, + floating_ip_settings=[create_instance.FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)]) + + self.inst_creator = create_instance.OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + except Exception as e: + self.tearDown() + raise Exception(e.message) + + def tearDown(self): + """ + Cleans the created objects + """ + if self.inst_creator: + self.inst_creator.clean() + + if self.keypair_creator: + self.keypair_creator.clean() + + if self.flavor_creator: + self.flavor_creator.clean() + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.router_creator: + self.router_creator.clean() + + if self.network_creator: + self.network_creator.clean() + + if self.image_creator: + self.image_creator.clean() + + if os.path.isfile(self.test_file_local_path): + os.remove(self.test_file_local_path) + + super(self.__class__, self).__clean__() + + def test_apply_simple_playbook(self): + """ + Tests application of an Ansible playbook that simply copies over a file: + 1. Have a ~/.ansible.cfg (or alternate means) to set host_key_checking = False + 2. Set the following environment variable in your executing shell: ANSIBLE_HOST_KEY_CHECKING=False + Should this not be performed, the creation of the host ssh key will cause your ansible calls to fail. + """ + self.inst_creator.create(block=True) + + # Block until VM's ssh port has been opened + self.assertTrue(self.inst_creator.vm_ssh_active(block=True)) + + ssh_client = self.inst_creator.ssh_client() + self.assertIsNotNone(ssh_client) + out = ssh_client.exec_command('pwd')[1].channel.in_buffer.read(1024) + self.assertIsNotNone(out) + self.assertGreater(len(out), 1) + + # Need to use the first floating IP as subsequent ones are currently broken with Apex CO + ip = self.inst_creator.get_floating_ip().ip + user = self.inst_creator.get_image_user() + priv_key = self.inst_creator.keypair_settings.private_filepath + + retval = ansible_utils.apply_playbook('provisioning/tests/playbooks/simple_playbook.yml', [ip], user, priv_key, + proxy_setting=self.os_creds.proxy_settings) + self.assertEquals(0, retval) + + ssh = ansible_utils.ssh_client(ip, user, priv_key, self.os_creds.proxy_settings) + self.assertIsNotNone(ssh) + scp = SCPClient(ssh.get_transport()) + scp.get('~/hello.txt', self.test_file_local_path) + + self.assertTrue(os.path.isfile(self.test_file_local_path)) + + with open(self.test_file_local_path) as f: + file_contents = f.readline() + self.assertEquals('Hello World!', file_contents) + + def test_apply_template_playbook(self): + """ + Tests application of an Ansible playbook that applies a template to a file: + 1. Have a ~/.ansible.cfg (or alternate means) to set host_key_checking = False + 2. Set the following environment variable in your executing shell: ANSIBLE_HOST_KEY_CHECKING=False + Should this not be performed, the creation of the host ssh key will cause your ansible calls to fail. + """ + self.inst_creator.create(block=True) + + # Block until VM's ssh port has been opened + self.assertTrue(self.inst_creator.vm_ssh_active(block=True)) + + # Need to use the first floating IP as subsequent ones are currently broken with Apex CO + ip = self.inst_creator.get_floating_ip().ip + user = self.inst_creator.get_image_user() + priv_key = self.inst_creator.keypair_settings.private_filepath + + ansible_utils.apply_playbook('provisioning/tests/playbooks/template_playbook.yml', [ip], user, priv_key, + variables={'name': 'Foo'}, proxy_setting=self.os_creds.proxy_settings) + + ssh = ansible_utils.ssh_client(ip, user, priv_key, self.os_creds.proxy_settings) + self.assertIsNotNone(ssh) + scp = SCPClient(ssh.get_transport()) + scp.get('/tmp/hello.txt', self.test_file_local_path) + + self.assertTrue(os.path.isfile(self.test_file_local_path)) + + with open(self.test_file_local_path) as f: + file_contents = f.readline() + self.assertEquals('Hello Foo!', file_contents) diff --git a/snaps/provisioning/tests/playbooks/simple_playbook.yml b/snaps/provisioning/tests/playbooks/simple_playbook.yml new file mode 100644 index 0000000..7af169c --- /dev/null +++ b/snaps/provisioning/tests/playbooks/simple_playbook.yml @@ -0,0 +1,21 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- hosts: all + + tasks: + - name: Transfer the test file + copy: src=../scripts/hello.txt dest=~/hello.txt mode=0777 + diff --git a/snaps/provisioning/tests/playbooks/template_playbook.yml b/snaps/provisioning/tests/playbooks/template_playbook.yml new file mode 100644 index 0000000..34d4e95 --- /dev/null +++ b/snaps/provisioning/tests/playbooks/template_playbook.yml @@ -0,0 +1,23 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +- hosts: all + become: yes + become_method: sudo + become_user: root + + tasks: + - name: Apply template and copy file + action: template owner=root group=root mode=777 src=../scripts/template.txt dest=/tmp/hello.txt diff --git a/snaps/provisioning/tests/scripts/hello.txt b/snaps/provisioning/tests/scripts/hello.txt new file mode 100644 index 0000000..c57eff5 --- /dev/null +++ b/snaps/provisioning/tests/scripts/hello.txt @@ -0,0 +1 @@ +Hello World!
\ No newline at end of file diff --git a/snaps/provisioning/tests/scripts/template.txt b/snaps/provisioning/tests/scripts/template.txt new file mode 100644 index 0000000..c7a43bc --- /dev/null +++ b/snaps/provisioning/tests/scripts/template.txt @@ -0,0 +1 @@ +Hello {{ name }}!
\ No newline at end of file diff --git a/snaps/test_suite_builder.py b/snaps/test_suite_builder.py new file mode 100644 index 0000000..32e0e35 --- /dev/null +++ b/snaps/test_suite_builder.py @@ -0,0 +1,208 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import unittest + +from snaps.openstack.utils.tests.glance_utils_tests import GlanceSmokeTests, GlanceUtilsTests +from snaps.openstack.tests.create_flavor_tests import CreateFlavorTests +from snaps.tests.file_utils_tests import FileUtilsTests +from snaps.openstack.tests.create_security_group_tests import CreateSecurityGroupTests, \ + SecurityGroupRuleSettingsUnitTests, SecurityGroupSettingsUnitTests +from snaps.openstack.tests.create_project_tests import CreateProjectSuccessTests, ProjectSettingsUnitTests, \ + CreateProjectUserTests +from snaps.openstack.tests.create_user_tests import UserSettingsUnitTests, CreateUserSuccessTests +from snaps.openstack.utils.tests.keystone_utils_tests import KeystoneSmokeTests, KeystoneUtilsTests +from snaps.openstack.utils.tests.neutron_utils_tests import NeutronSmokeTests, NeutronUtilsNetworkTests, \ + NeutronUtilsSubnetTests, NeutronUtilsRouterTests, NeutronUtilsSecurityGroupTests +from snaps.openstack.tests.create_image_tests import CreateImageSuccessTests, CreateImageNegativeTests, \ + ImageSettingsUnitTests +from snaps.openstack.tests.create_keypairs_tests import CreateKeypairsTests, KeypairSettingsUnitTests +from snaps.openstack.tests.create_network_tests import CreateNetworkSuccessTests, NetworkSettingsUnitTests, \ + PortSettingsUnitTests, SubnetSettingsUnitTests, CreateNetworkTypeTests +from snaps.openstack.tests.create_router_tests import CreateRouterSuccessTests, CreateRouterNegativeTests +from snaps.openstack.tests.create_instance_tests import CreateInstanceSingleNetworkTests, \ + CreateInstancePubPrivNetTests, CreateInstanceOnComputeHost, CreateInstanceSimpleTests, \ + FloatingIpSettingsUnitTests, InstanceSecurityGroupTests, VmInstanceSettingsUnitTests, \ + CreateInstancePortManipulationTests, SimpleHealthCheck +from snaps.provisioning.tests.ansible_utils_tests import AnsibleProvisioningTests +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase, OSIntegrationTestCase +from snaps.openstack.utils.tests.nova_utils_tests import NovaSmokeTests, NovaUtilsKeypairTests, NovaUtilsFlavorTests + +__author__ = 'spisarski' + + +def add_unit_tests(suite): + """ + Adds tests that do not require external resources + :param suite: the unittest.TestSuite object to which to add the tests + :return: None as the tests will be adding to the 'suite' parameter object + """ + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(FileUtilsTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SecurityGroupRuleSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SecurityGroupSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ImageSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(KeypairSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(UserSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ProjectSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(NetworkSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SubnetSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(PortSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(FloatingIpSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(VmInstanceSettingsUnitTests)) + + +def add_openstack_client_tests(suite, source_filename, ext_net_name, use_keystone=True, http_proxy_str=None, + log_level=logging.INFO): + """ + Adds tests written to exercise OpenStack client retrieval + :param suite: the unittest.TestSuite object to which to add the tests + :param source_filename: the OpenStack credentials filename + :param ext_net_name: the name of an external network on the cloud under test + :param http_proxy_str: <host>:<port> of the proxy server (optional) + :param use_keystone: when True, tests requiring direct access to Keystone are added as these need to be running on + a host that has access to the cloud's private network + :param log_level: the logging level + :return: None as the tests will be adding to the 'suite' parameter object + """ + # Basic connection tests + suite.addTest(OSComponentTestCase.parameterize(GlanceSmokeTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + + if use_keystone: + suite.addTest(OSComponentTestCase.parameterize(KeystoneSmokeTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + + suite.addTest(OSComponentTestCase.parameterize(NeutronSmokeTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(NovaSmokeTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + + +def add_openstack_api_tests(suite, source_filename, ext_net_name, http_proxy_str=None, use_keystone=True, + log_level=logging.INFO): + """ + Adds tests written to exercise all existing OpenStack APIs + :param suite: the unittest.TestSuite object to which to add the tests + :param source_filename: the OpenStack credentials filename + :param ext_net_name: the name of an external network on the cloud under test + :param http_proxy_str: <host>:<port> of the proxy server (optional) + :param use_keystone: when True, tests requiring direct access to Keystone are added as these need to be running on + a host that has access to the cloud's private network + :param log_level: the logging level + :return: None as the tests will be adding to the 'suite' parameter object + """ + # Tests the OpenStack API calls + if use_keystone: + suite.addTest(OSComponentTestCase.parameterize(KeystoneUtilsTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(CreateUserSuccessTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(CreateProjectSuccessTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(CreateProjectUserTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + + suite.addTest(OSComponentTestCase.parameterize(GlanceUtilsTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsNetworkTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsSubnetTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsRouterTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsSecurityGroupTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(NovaUtilsKeypairTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(NovaUtilsFlavorTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(CreateFlavorTests, source_filename, ext_net_name, + http_proxy_str=http_proxy_str, log_level=log_level)) + + +def add_openstack_integration_tests(suite, source_filename, ext_net_name, proxy_settings=None, ssh_proxy_cmd=None, + use_keystone=True, use_floating_ips=True, log_level=logging.INFO): + """ + Adds tests written to exercise all long-running OpenStack integration tests meaning they will be creating VM + instances and potentially performing some SSH functions through floating IPs + :param suite: the unittest.TestSuite object to which to add the tests + :param source_filename: the OpenStack credentials filename + :param ext_net_name: the name of an external network on the cloud under test + :param proxy_settings: <host>:<port> of the proxy server (optional) + :param ssh_proxy_cmd: the command your environment requires for creating ssh connections through a proxy + :param use_keystone: when True, tests requiring direct access to Keystone are added as these need to be running on + a host that has access to the cloud's private network + :param use_floating_ips: when true, all tests requiring Floating IPs will be added to the suite + :param log_level: the logging level + :return: None as the tests will be adding to the 'suite' parameter object + """ + # Tests the OpenStack API calls via a creator. If use_keystone, objects will be created with a custom user + # and project + + # Creator Object tests + suite.addTest(OSIntegrationTestCase.parameterize(CreateSecurityGroupTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateImageSuccessTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateImageNegativeTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateKeypairsTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateNetworkSuccessTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(CreateNetworkTypeTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateRouterSuccessTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateRouterNegativeTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + + # VM Instances + suite.addTest(OSIntegrationTestCase.parameterize(SimpleHealthCheck, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateInstanceSimpleTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateInstancePortManipulationTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(InstanceSecurityGroupTests, source_filename, ext_net_name, + http_proxy_str=proxy_settings, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize(CreateInstanceOnComputeHost, source_filename, ext_net_name, + http_proxy_str=proxy_settings, log_level=log_level)) + + if use_floating_ips: + suite.addTest(OSIntegrationTestCase.parameterize(CreateInstanceSingleNetworkTests, source_filename, + ext_net_name, http_proxy_str=proxy_settings, + ssh_proxy_cmd=ssh_proxy_cmd, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(CreateInstancePubPrivNetTests, source_filename, + ext_net_name, http_proxy_str=proxy_settings, + ssh_proxy_cmd=ssh_proxy_cmd, use_keystone=use_keystone, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize(AnsibleProvisioningTests, source_filename, + ext_net_name, http_proxy_str=proxy_settings, + ssh_proxy_cmd=ssh_proxy_cmd, use_keystone=use_keystone, + log_level=log_level)) diff --git a/snaps/tests/__init__.py b/snaps/tests/__init__.py new file mode 100644 index 0000000..e3e876e --- /dev/null +++ b/snaps/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' diff --git a/snaps/tests/file_utils_tests.py b/snaps/tests/file_utils_tests.py new file mode 100644 index 0000000..d517d5d --- /dev/null +++ b/snaps/tests/file_utils_tests.py @@ -0,0 +1,102 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import unittest +import shutil +import uuid + +from snaps import file_utils + +__author__ = 'spisarski' + + +class FileUtilsTests(unittest.TestCase): + """ + Tests the methods in file_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.tmpDir = 'tmp/' + str(guid) + if not os.path.exists(self.tmpDir): + os.makedirs(self.tmpDir) + + self.tmpFile = self.tmpDir + '/bar.txt' + if not os.path.exists(self.tmpFile): + open(self.tmpFile, 'wb') + + def tearDown(self): + if os.path.exists(self.tmpDir) and os.path.isdir(self.tmpDir): + shutil.rmtree(self.tmpDir) + + def testFileIsDirectory(self): + """ + Ensure the file_utils.fileExists() method returns false with a directory + """ + result = file_utils.file_exists(self.tmpDir) + self.assertFalse(result) + # TODO - Cleanup directory + + def testFileNotExist(self): + """ + Ensure the file_utils.fileExists() method returns false with a bogus file + """ + result = file_utils.file_exists('/foo/bar.txt') + self.assertFalse(result) + + def testFileExists(self): + """ + Ensure the file_utils.fileExists() method returns false with a directory + """ + if not os.path.exists(self.tmpFile): + os.makedirs(self.tmpFile) + + result = file_utils.file_exists(self.tmpFile) + self.assertTrue(result) + + def testDownloadBadUrl(self): + """ + Tests the file_utils.download() method when given a bad URL + """ + with self.assertRaises(Exception): + file_utils.download('http://bunkUrl.com/foo/bar.iso', self.tmpDir) + + def testDownloadBadDir(self): + """ + Tests the file_utils.download() method when given a bad URL + """ + with self.assertRaises(Exception): + file_utils.download('http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img', '/foo/bar') + + def testCirrosImageDownload(self): + """ + Tests the file_utils.download() method when given a good Cirros QCOW2 URL + """ + image_file = file_utils.download('http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img', + self.tmpDir) + self.assertIsNotNone(image_file) + self.assertTrue(image_file.name.endswith("cirros-0.3.4-x86_64-disk.img")) + self.assertTrue(image_file.name.startswith(self.tmpDir)) + + def testReadOSEnvFile(self): + """ + Tests that the OS Environment file is correctly parsed + :return: + """ + os_env_dict = file_utils.read_os_env_file('openstack/tests/conf/overcloudrc_test') + self.assertEquals('test_pw', os_env_dict['OS_PASSWORD']) + self.assertEquals('http://foo:5000/v2.0/', os_env_dict['OS_AUTH_URL']) + self.assertEquals('admin', os_env_dict['OS_USERNAME']) + self.assertEquals('admin', os_env_dict['OS_TENANT_NAME']) diff --git a/snaps/unit_test_suite.py b/snaps/unit_test_suite.py new file mode 100644 index 0000000..720d547 --- /dev/null +++ b/snaps/unit_test_suite.py @@ -0,0 +1,131 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import unittest +import os + +from snaps import test_suite_builder + +__author__ = 'spisarski' + +logger = logging.getLogger('unit_test_suite') + +ARG_NOT_SET = "argument not set" +LOG_LEVELS = {'FATAL': logging.FATAL, 'CRITICAL': logging.CRITICAL, 'ERROR': logging.ERROR, 'WARN': logging.WARN, + 'INFO': logging.INFO, 'DEBUG': logging.DEBUG} + + +def __create_test_suite(source_filename, ext_net_name, proxy_settings, ssh_proxy_cmd, run_unit_tests, use_keystone, + use_floating_ips, log_level): + """ + Compiles the tests that should run + :param source_filename: the OpenStack credentials file (required) + :param ext_net_name: the name of the external network to use for floating IPs (required) + :param run_unit_tests: when true, the tests not requiring OpenStack will be added to the test suite + :param proxy_settings: <host>:<port> of the proxy server (optional) + :param ssh_proxy_cmd: the command used to connect via SSH over some proxy server (optional) + :param use_keystone: when true, tests creating users and projects will be exercised and must be run on a host that + has access to the cloud's administrative network + :param use_floating_ips: when true, tests requiring floating IPs will be executed + :param log_level: the logging level + :return: + """ + suite = unittest.TestSuite() + + # Tests that do not require a remote connection to an OpenStack cloud + if run_unit_tests: + test_suite_builder.add_unit_tests(suite) + + # Basic connection tests + test_suite_builder.add_openstack_client_tests(suite, source_filename, ext_net_name, use_keystone=use_keystone, + http_proxy_str=proxy_settings, log_level=log_level) + + # Tests the OpenStack API calls + test_suite_builder.add_openstack_api_tests(suite, source_filename, ext_net_name, use_keystone=use_keystone, + http_proxy_str=proxy_settings, log_level=log_level) + + # Long running integration type tests + test_suite_builder.add_openstack_integration_tests(suite, source_filename, ext_net_name, use_keystone=use_keystone, + proxy_settings=proxy_settings, ssh_proxy_cmd=ssh_proxy_cmd, + use_floating_ips=use_floating_ips, log_level=log_level) + return suite + + +def main(arguments): + """ + Begins running unit tests. + argv[1] if used must be the source filename else os_env.yaml will be leveraged instead + argv[2] if used must be the proxy server <host>:<port> + """ + logger.info('Starting test suite') + + log_level = LOG_LEVELS.get(arguments.log_level, logging.DEBUG) + + suite = None + if arguments.env and arguments.ext_net: + suite = __create_test_suite(arguments.env, arguments.ext_net, arguments.proxy, arguments.ssh_proxy_cmd, + arguments.include_units != ARG_NOT_SET, + arguments.use_keystone != ARG_NOT_SET, + arguments.no_floating_ips == ARG_NOT_SET, log_level) + else: + logger.error('Environment file or external network not defined') + exit(1) + + # To ensure any files referenced via a relative path will begin from the diectory in which this file resides + os.chdir(os.path.dirname(os.path.realpath(__file__))) + + result = unittest.TextTestRunner(verbosity=2).run(suite) + + if result.errors: + logger.error('Number of errors in test suite - ' + str(len(result.errors))) + for test, message in result.errors: + logger.error(str(test) + " ERROR with " + message) + + if result.failures: + logger.error('Number of failures in test suite - ' + str(len(result.failures))) + for test, message in result.failures: + logger.error(str(test) + " FAILED with " + message) + + if (result.errors and len(result.errors) > 0) or (result.failures and len(result.failures) > 0): + logger.error('See above for test failures') + exit(1) + else: + logger.info('All tests completed successfully') + + exit(0) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-e', '--env', dest='env', required=True, help='OpenStack credentials file') + parser.add_argument('-n', '--net', dest='ext_net', required=True, help='External network name') + parser.add_argument('-p', '--proxy', dest='proxy', nargs='?', default=None, + help='Optonal HTTP proxy socket (<host>:<port>)') + parser.add_argument('-s', '--ssh-proxy-cmd', dest='ssh_proxy_cmd', nargs='?', default=None, + help='Optonal SSH proxy command value') + parser.add_argument('-l', '--log-level', dest='log_level', default='INFO', + help='Logging Level (FATAL|CRITICAL|ERROR|WARN|INFO|DEBUG)') + parser.add_argument('-k', '--use-keystone', dest='use_keystone', default=ARG_NOT_SET, nargs='?', + help='When argument is set, the tests will exercise the keystone APIs and must be run on a ' + + 'machine that has access to the admin network' + + ' and is able to create users and groups') + parser.add_argument('-f', '--no-floating-ips', dest='no_floating_ips', default=ARG_NOT_SET, nargs='?', + help='When argument is set, all tests requiring Floating IPs will not be executed') + parser.add_argument('-u', '--include-units', dest='include_units', default=ARG_NOT_SET, nargs='?', + help='When argument is set, all tests not requiring OpenStack will be executed') + args = parser.parse_args() + + main(args) |