diff options
31 files changed, 1923 insertions, 263 deletions
diff --git a/docs/how-to-use/APITests.rst b/docs/how-to-use/APITests.rst index 0d4239f..4a8035a 100644 --- a/docs/how-to-use/APITests.rst +++ b/docs/how-to-use/APITests.rst @@ -318,12 +318,52 @@ create_flavor_tests.py - CreateFlavorTests | | | a flavor properly with all supported settings | +---------------------------------------+---------------+-----------------------------------------------------------+ -heat_utils_tests.py - HeatUtilsCreateStackTests ------------------------------------------------ +heat_utils_tests.py - HeatUtilsCreateSimpleStackTests +----------------------------------------------------- +---------------------------------------+---------------+-----------------------------------------------------------+ -| Test Name | Glance API | Description | +| Test Name | Heat API | Description | +=======================================+===============+===========================================================+ | test_create_stack | 1 | Tests the heat_utils.create_stack() with a test template | +---------------------------------------+---------------+-----------------------------------------------------------+ +| test_create_stack_x2 | 1 | Tests the heat_utils.create_stack() with a test template | +| | | and attempts to deploy a second time w/o actually | +| | | deploying any objects | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +heat_utils_tests.py - HeatUtilsCreateComplexStackTests +------------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Heat API | Description | ++=======================================+===============+===========================================================+ +| test_get_settings_from_stack | 1 | Tests the heat_utils functions that are responsible for | +| | | reverse engineering settings objects of the types deployed| +| | | by Heat | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +settings_utils_tests.py - SettingsUtilsNetworkingTests +------------------------------------------------------ + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API | Description | ++=======================================+===============+===========================================================+ +| test_derive_net_settings_no_subnet | Neutron 2 | Tests to ensure that derived NetworkSettings from an | +| | | OpenStack network are correct without a subnet | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_derive_net_settings_two_subnets | Neutron 2 | Tests to ensure that derived NetworkSettings from an | +| | | OpenStack network are correct with two subnets | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +settings_utils_tests.py - SettingsUtilsVmInstTests +-------------------------------------------------- ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | API | Description | ++=======================================+===============+===========================================================+ +| test_derive_vm_inst_settings | Neutron 2 | Tests to ensure that derived VmInstanceSettings from an | +| | | OpenStack VM instance is correct | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_derive_image_settings | Neutron 2 | Tests to ensure that derived ImageSettings from an | +| | | OpenStack VM instance is correct | ++---------------------------------------+---------------+-----------------------------------------------------------+ diff --git a/docs/how-to-use/IntegrationTests.rst b/docs/how-to-use/IntegrationTests.rst index 8ef54ec..5b4830e 100644 --- a/docs/how-to-use/IntegrationTests.rst +++ b/docs/how-to-use/IntegrationTests.rst @@ -247,8 +247,22 @@ create_stack_tests.py - CreateStackSuccessTests | test_create_same_stack | 2 | Ensures that a Heat stack with the same name cannot be | | | | created 2x | +---------------------------------------+---------------+-----------------------------------------------------------+ -| test_create_same_stack | 2 | Ensures that a Heat stack with the same name cannot be | -| | | created 2x | +| test_retrieve_network_creators | 2 | Ensures that an OpenStackHeatStack instance can return an | +| | | OpenStackNetwork instance configured as deployed | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_retrieve_vm_inst_creators | 2 | Ensures that an OpenStackHeatStack instance can return an | +| | | OpenStackVmInstance instance configured as deployed | ++---------------------------------------+---------------+-----------------------------------------------------------+ + +create_stack_tests.py - CreateComplexStackTests +----------------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Neutron API | Description | ++=======================================+===============+===========================================================+ +| test_connect_via_ssh_heat_vm | 2 | Ensures that two OpenStackHeatStack instances can return | +| | | OpenStackVmInstance instances one configured with a | +| | | floating IP and keypair and can be access via SSH | +---------------------------------------+---------------+-----------------------------------------------------------+ create_stack_tests.py - CreateStackNegativeTests diff --git a/docs/how-to-use/UnitTests.rst b/docs/how-to-use/UnitTests.rst index 6f4dd6c..5fb04db 100644 --- a/docs/how-to-use/UnitTests.rst +++ b/docs/how-to-use/UnitTests.rst @@ -208,13 +208,19 @@ StackDomainObjectTests ---------------------- Ensures that all required members are included when constructing a -Stack domain object +Stack domain object (for Heat) ResourceDomainObjectTests ------------------------- Ensures that all required members are included when constructing a -Resource domain object +Resource domain object (for Heat) + +OutputDomainObjectTests +----------------------- + +Ensures that all required members are included when constructing a +Output domain object (for Heat) FloatingIpSettingsUnitTests --------------------------- diff --git a/snaps/domain/keypair.py b/snaps/domain/keypair.py index 2865125..5e169fb 100644 --- a/snaps/domain/keypair.py +++ b/snaps/domain/keypair.py @@ -19,15 +19,18 @@ class Keypair: SNAPS domain object for Keypairs. Should contain attributes that are shared amongst cloud providers """ - def __init__(self, name, id, public_key): + def __init__(self, name, kp_id, public_key, fingerprint=None): """ Constructor :param name: the keypair's name - :param id: the keypair's id + :param kp_id: the keypair's id + :param public_key: the keypair's public key + :param fingerprint: the keypair's host fingerprint """ self.name = name - self.id = id + self.id = kp_id self.public_key = public_key + self.fingerprint = fingerprint def __eq__(self, other): return (self.name == other.name and self.id == other.id and diff --git a/snaps/domain/network.py b/snaps/domain/network.py index 0b56c43..9cc1dd1 100644 --- a/snaps/domain/network.py +++ b/snaps/domain/network.py @@ -92,13 +92,32 @@ class Port: Constructor :param name: the security group's name :param id: the security group's id - :param ips: a list of IP addresses + :param description: description + :param ips|fixed_ips: a list of IP addresses + :param mac_address: the port's MAC addresses + :param allowed_address_pairs: the port's allowed_address_pairs value + :param admin_state_up: T|F whether or not the port is up + :param device_id: device's ID + :param device_owner: device's owner + :param network_id: associated network ID + :param port_security_enabled: T|F whether or not the port security is + enabled + :param security_groups: the security group IDs associated with port + :param project_id: the associated project/tenant ID """ self.name = kwargs.get('name') self.id = kwargs.get('id') - self.ips = kwargs.get('ips') + self.description = kwargs.get('description') + self.ips = kwargs.get('ips', kwargs.get('fixed_ips')) self.mac_address = kwargs.get('mac_address') self.allowed_address_pairs = kwargs.get('allowed_address_pairs') + self.admin_state_up = kwargs.get('admin_state_up') + self.device_id = kwargs.get('device_id') + self.device_owner = kwargs.get('device_owner') + self.network_id = kwargs.get('network_id') + self.port_security_enabled = kwargs.get('port_security_enabled') + self.security_groups = kwargs.get('security_groups') + self.project_id = kwargs.get('tenant_id', kwargs.get('project_id')) def __eq__(self, other): return (self.name == other.name and self.id == other.id and diff --git a/snaps/domain/stack.py b/snaps/domain/stack.py index df4d4e4..543c78b 100644 --- a/snaps/domain/stack.py +++ b/snaps/domain/stack.py @@ -35,7 +35,7 @@ class Stack: class Resource: """ - SNAPS domain object for resources created by a heat template + SNAPS domain object for a resource created by a heat template """ def __init__(self, resource_type, resource_id): """ @@ -45,3 +45,18 @@ class Resource: """ self.type = resource_type self.id = resource_id + + +class Output: + """ + SNAPS domain object for an output defined by a heat template + """ + def __init__(self, **kwargs): + """ + Constructor + :param description: the output description + :param output_key: the output's key + """ + self.description = kwargs.get('description') + self.key = kwargs.get('output_key') + self.value = kwargs.get('output_value') diff --git a/snaps/domain/test/keypair_tests.py b/snaps/domain/test/keypair_tests.py index 93f99ff..1cb9f91 100644 --- a/snaps/domain/test/keypair_tests.py +++ b/snaps/domain/test/keypair_tests.py @@ -23,13 +23,16 @@ class KeypairDomainObjectTests(unittest.TestCase): """ def test_construction_positional(self): - keypair = Keypair('foo', '123-456', 'foo-bar') + keypair = Keypair('foo', '123-456', 'foo-bar', '01:02:03') self.assertEqual('foo', keypair.name) self.assertEqual('123-456', keypair.id) self.assertEqual('foo-bar', keypair.public_key) + self.assertEqual('01:02:03', keypair.fingerprint) def test_construction_named(self): - keypair = Keypair(public_key='foo-bar', id='123-456', name='foo') + keypair = Keypair(fingerprint='01:02:03', public_key='foo-bar', + kp_id='123-456', name='foo') self.assertEqual('foo', keypair.name) self.assertEqual('123-456', keypair.id) self.assertEqual('foo-bar', keypair.public_key) + self.assertEqual('01:02:03', keypair.fingerprint) diff --git a/snaps/domain/test/network_tests.py b/snaps/domain/test/network_tests.py index 0534b49..24a60c9 100644 --- a/snaps/domain/test/network_tests.py +++ b/snaps/domain/test/network_tests.py @@ -107,20 +107,95 @@ class PortDomainObjectTests(unittest.TestCase): Tests the construction of the snaps.domain.network.Port class """ - def test_construction_kwargs(self): + def test_construction_ips_kwargs(self): ips = ['10', '11'] port = Port( - **{'name': 'name', 'id': 'id', 'ips': ips}) - self.assertEqual('name', port.name) - self.assertEqual('id', port.id) + **{'name': 'foo', 'id': 'bar', 'description': 'test desc', + 'ips': ips, 'mac_address': 'abc123', + 'allowed_address_pairs': list(), 'admin_state_up': False, + 'device_id': 'def456', 'device_owner': 'joe', + 'network_id': 'ghi789', 'port_security_enabled': False, + 'security_groups': list(), 'tenant_id': 'jkl098'}) + self.assertEqual('foo', port.name) + self.assertEqual('bar', port.id) + self.assertEqual('test desc', port.description) self.assertEqual(ips, port.ips) + self.assertEqual('abc123', port.mac_address) + self.assertEqual(list(), port.allowed_address_pairs) + self.assertFalse(port.admin_state_up) + self.assertEqual('def456', port.device_id) + self.assertEqual('joe', port.device_owner) + self.assertEqual('ghi789', port.network_id) + self.assertFalse(port.port_security_enabled) + self.assertEqual(list(), port.security_groups) + self.assertEqual(list(), port.security_groups) - def test_construction_named(self): + def test_construction_fixed_ips_kwargs(self): + ips = ['10', '11'] + port = Port( + **{'name': 'foo', 'id': 'bar', 'description': 'test desc', + 'fixed_ips': ips, 'mac_address': 'abc123', + 'allowed_address_pairs': list(), 'admin_state_up': False, + 'device_id': 'def456', 'device_owner': 'joe', + 'network_id': 'ghi789', 'port_security_enabled': False, + 'security_groups': list(), 'tenant_id': 'jkl098'}) + self.assertEqual('foo', port.name) + self.assertEqual('bar', port.id) + self.assertEqual('test desc', port.description) + self.assertEqual(ips, port.ips) + self.assertEqual('abc123', port.mac_address) + self.assertEqual(list(), port.allowed_address_pairs) + self.assertFalse(port.admin_state_up) + self.assertEqual('def456', port.device_id) + self.assertEqual('joe', port.device_owner) + self.assertEqual('ghi789', port.network_id) + self.assertFalse(port.port_security_enabled) + self.assertEqual(list(), port.security_groups) + self.assertEqual(list(), port.security_groups) + + def test_construction_named_ips(self): ips = ['10', '11'] - port = Port(ips=ips, id='id', name='name') - self.assertEqual('name', port.name) - self.assertEqual('id', port.id) + port = Port( + mac_address='abc123', description='test desc', ips=ips, id='bar', + name='foo', allowed_address_pairs=list(), admin_state_up=False, + device_id='def456', device_owner='joe', network_id='ghi789', + port_security_enabled=False, security_groups=list(), + tenant_id='jkl098') + self.assertEqual('foo', port.name) + self.assertEqual('bar', port.id) + self.assertEqual('test desc', port.description) + self.assertEqual(ips, port.ips) + self.assertEqual('abc123', port.mac_address) + self.assertEqual(list(), port.allowed_address_pairs) + self.assertFalse(port.admin_state_up) + self.assertEqual('def456', port.device_id) + self.assertEqual('joe', port.device_owner) + self.assertEqual('ghi789', port.network_id) + self.assertFalse(port.port_security_enabled) + self.assertEqual(list(), port.security_groups) + self.assertEqual(list(), port.security_groups) + + def test_construction_named_fixed_ips(self): + ips = ['10', '11'] + port = Port( + mac_address='abc123', description='test desc', fixed_ips=ips, + id='bar', name='foo', allowed_address_pairs=list(), + admin_state_up=False, device_id='def456', device_owner='joe', + network_id='ghi789', port_security_enabled=False, + security_groups=list(), tenant_id='jkl098') + self.assertEqual('foo', port.name) + self.assertEqual('bar', port.id) + self.assertEqual('test desc', port.description) self.assertEqual(ips, port.ips) + self.assertEqual('abc123', port.mac_address) + self.assertEqual(list(), port.allowed_address_pairs) + self.assertFalse(port.admin_state_up) + self.assertEqual('def456', port.device_id) + self.assertEqual('joe', port.device_owner) + self.assertEqual('ghi789', port.network_id) + self.assertFalse(port.port_security_enabled) + self.assertEqual(list(), port.security_groups) + self.assertEqual(list(), port.security_groups) class RouterDomainObjectTests(unittest.TestCase): diff --git a/snaps/domain/test/stack_tests.py b/snaps/domain/test/stack_tests.py index e0e1ae7..f816ef8 100644 --- a/snaps/domain/test/stack_tests.py +++ b/snaps/domain/test/stack_tests.py @@ -14,7 +14,7 @@ # limitations under the License. import unittest -from snaps.domain.stack import Stack, Resource +from snaps.domain.stack import Stack, Resource, Output class StackDomainObjectTests(unittest.TestCase): @@ -47,3 +47,24 @@ class ResourceDomainObjectTests(unittest.TestCase): resource = Resource(resource_id='bar', resource_type='foo') self.assertEqual('foo', resource.type) self.assertEqual('bar', resource.id) + + +class OutputDomainObjectTests(unittest.TestCase): + """ + Tests the construction of the snaps.domain.Resource class + """ + + def test_construction_kwargs(self): + kwargs = {'description': 'foo', 'output_key': 'test_key', + 'output_value': 'bar'} + resource = Output(**kwargs) + self.assertEqual('foo', resource.description) + self.assertEqual('test_key', resource.key) + self.assertEqual('bar', resource.value) + + def test_construction_named(self): + resource = Output(description='foo', output_key='test_key', + output_value='bar') + self.assertEqual('foo', resource.description) + self.assertEqual('test_key', resource.key) + self.assertEqual('bar', resource.value) diff --git a/snaps/domain/test/vm_inst_tests.py b/snaps/domain/test/vm_inst_tests.py index c3de8ba..d293373 100644 --- a/snaps/domain/test/vm_inst_tests.py +++ b/snaps/domain/test/vm_inst_tests.py @@ -23,16 +23,26 @@ class VmInstDomainObjectTests(unittest.TestCase): """ def test_construction_positional(self): - vm_inst = VmInst('name', 'id', dict()) + vm_inst = VmInst('name', 'id', '456', '123', dict(), 'kp-name', list()) self.assertEqual('name', vm_inst.name) self.assertEqual('id', vm_inst.id) + self.assertEqual('456', vm_inst.image_id) + self.assertEqual('123', vm_inst.flavor_id) self.assertEqual(dict(), vm_inst.networks) + self.assertEqual('kp-name', vm_inst.keypair_name) + self.assertEqual(list(), vm_inst.sec_grp_names) def test_construction_named(self): - vm_inst = VmInst(networks=dict(), inst_id='id', name='name') + vm_inst = VmInst(sec_grp_names=list(), networks=dict(), inst_id='id', + name='name', flavor_id='123', image_id='456', + keypair_name='kp-name') self.assertEqual('name', vm_inst.name) self.assertEqual('id', vm_inst.id) + self.assertEqual('456', vm_inst.image_id) + self.assertEqual('123', vm_inst.flavor_id) self.assertEqual(dict(), vm_inst.networks) + self.assertEqual('kp-name', vm_inst.keypair_name) + self.assertEqual(list(), vm_inst.sec_grp_names) class FloatingIpDomainObjectTests(unittest.TestCase): @@ -40,12 +50,63 @@ class FloatingIpDomainObjectTests(unittest.TestCase): Tests the construction of the snaps.domain.test.Image class """ - def test_construction_positional(self): - vm_inst = FloatingIp('id-123', '10.0.0.1') - self.assertEqual('id-123', vm_inst.id) - self.assertEqual('10.0.0.1', vm_inst.ip) + def test_construction_kwargs_ip_proj(self): + kwargs = {'id': 'foo', 'description': 'bar', 'ip': '192.168.122.3', + 'fixed_ip_address': '10.0.0.3', + 'floating_network_id': 'id_of_net', 'port_id': 'id_of_port', + 'router_id': 'id_of_router', 'project_id': 'id_of_proj'} + vm_inst = FloatingIp(**kwargs) + self.assertEqual('foo', vm_inst.id) + self.assertEqual('bar', vm_inst.description) + self.assertEqual('192.168.122.3', vm_inst.ip) + self.assertEqual('10.0.0.3', vm_inst.fixed_ip_address) + self.assertEqual('id_of_net', vm_inst.floating_network_id) + self.assertEqual('id_of_port', vm_inst.port_id) + self.assertEqual('id_of_router', vm_inst.router_id) + self.assertEqual('id_of_proj', vm_inst.project_id) - def test_construction_named(self): - vm_inst = FloatingIp(ip='10.0.0.1', inst_id='id-123') - self.assertEqual('id-123', vm_inst.id) - self.assertEqual('10.0.0.1', vm_inst.ip) + def test_construction_kwargs_fixed_ip_tenant(self): + kwargs = {'id': 'foo', 'description': 'bar', + 'floating_ip_address': '192.168.122.3', + 'fixed_ip_address': '10.0.0.3', + 'floating_network_id': 'id_of_net', 'port_id': 'id_of_port', + 'router_id': 'id_of_router', 'tenant_id': 'id_of_proj'} + vm_inst = FloatingIp(**kwargs) + self.assertEqual('foo', vm_inst.id) + self.assertEqual('bar', vm_inst.description) + self.assertEqual('192.168.122.3', vm_inst.ip) + self.assertEqual('10.0.0.3', vm_inst.fixed_ip_address) + self.assertEqual('id_of_net', vm_inst.floating_network_id) + self.assertEqual('id_of_port', vm_inst.port_id) + self.assertEqual('id_of_router', vm_inst.router_id) + self.assertEqual('id_of_proj', vm_inst.project_id) + + def test_construction_named_ip_proj(self): + vm_inst = FloatingIp( + id='foo', description='bar', ip='192.168.122.3', + fixed_ip_address='10.0.0.3', floating_network_id='id_of_net', + port_id='id_of_port', router_id='id_of_router', + project_id='id_of_proj') + self.assertEqual('foo', vm_inst.id) + self.assertEqual('bar', vm_inst.description) + self.assertEqual('192.168.122.3', vm_inst.ip) + self.assertEqual('10.0.0.3', vm_inst.fixed_ip_address) + self.assertEqual('id_of_net', vm_inst.floating_network_id) + self.assertEqual('id_of_port', vm_inst.port_id) + self.assertEqual('id_of_router', vm_inst.router_id) + self.assertEqual('id_of_proj', vm_inst.project_id) + + def test_construction_kwargs_named_fixed_ip_tenant(self): + vm_inst = FloatingIp( + id='foo', description='bar', floating_ip_address='192.168.122.3', + fixed_ip_address='10.0.0.3', floating_network_id='id_of_net', + port_id='id_of_port', router_id='id_of_router', + tenant_id='id_of_proj') + self.assertEqual('foo', vm_inst.id) + self.assertEqual('bar', vm_inst.description) + self.assertEqual('192.168.122.3', vm_inst.ip) + self.assertEqual('10.0.0.3', vm_inst.fixed_ip_address) + self.assertEqual('id_of_net', vm_inst.floating_network_id) + self.assertEqual('id_of_port', vm_inst.port_id) + self.assertEqual('id_of_router', vm_inst.router_id) + self.assertEqual('id_of_proj', vm_inst.project_id) diff --git a/snaps/domain/vm_inst.py b/snaps/domain/vm_inst.py index ae01cf0..ca38143 100644 --- a/snaps/domain/vm_inst.py +++ b/snaps/domain/vm_inst.py @@ -19,17 +19,35 @@ class VmInst: SNAPS domain object for Images. Should contain attributes that are shared amongst cloud providers """ - def __init__(self, name, inst_id, networks): + def __init__(self, name, inst_id, image_id, flavor_id, networks, + keypair_name, sec_grp_names): """ Constructor :param name: the image's name :param inst_id: the instance's id - :param networks: dict of networks where the key is the subnet name and + :param image_id: the instance's image id + :param flavor_id: the ID used to spawn this instance + :param networks: dict of networks where the key is the network name and value is a list of associated IPs + :param keypair_name: the name of the associated keypair + :param sec_grp_names: list of security group names """ self.name = name self.id = inst_id + self.image_id = image_id + self.flavor_id = flavor_id self.networks = networks + self.keypair_name = keypair_name + self.sec_grp_names = sec_grp_names + + def __eq__(self, other): + return (self.name == other.name and + self.id == other.id and + self.image_id == other.image_id and + self.flavor_id == other.flavor_id and + self.networks == other.networks and + self.keypair_name == other.keypair_name and + self.sec_grp_names == other.sec_grp_names) class FloatingIp: @@ -37,11 +55,26 @@ class FloatingIp: SNAPS domain object for Images. Should contain attributes that are shared amongst cloud providers """ - def __init__(self, inst_id, ip): + def __init__(self, **kwargs): """ Constructor - :param inst_id: the floating ip's id - :param ip: the IP address + :param id: the floating ip's id + :param description: the description + :param ip|floating_ip_address: the Floating IP address mapped to the + 'ip' attribute + :param fixed_ip_address: the IP address of the tenant network + :param floating_network_id: the ID of the external network + :param port_id: the ID of the associated port + :param router_id: the ID of the associated router + :param project_id|tenant_id: the ID of the associated project mapped to + the attribute 'project_id' + :param """ - self.id = inst_id - self.ip = ip + self.id = kwargs.get('id') + self.description = kwargs.get('description') + self.ip = kwargs.get('ip', kwargs.get('floating_ip_address')) + self.fixed_ip_address = kwargs.get('fixed_ip_address') + self.floating_network_id = kwargs.get('floating_network_id') + self.port_id = kwargs.get('port_id') + self.router_id = kwargs.get('router_id') + self.project_id = kwargs.get('project_id', kwargs.get('tenant_id')) diff --git a/snaps/file_utils.py b/snaps/file_utils.py index ff2f1b3..699d378 100644 --- a/snaps/file_utils.py +++ b/snaps/file_utils.py @@ -14,6 +14,9 @@ # limitations under the License. import os import logging + +from cryptography.hazmat.primitives import serialization + try: import urllib.request as urllib except ImportError: @@ -65,7 +68,8 @@ def download(url, dest_path, name=None): raise try: with open(dest, 'wb') as download_file: - logger.debug('Saving file to - ' + os.path.abspath(download_file.name)) + logger.debug('Saving file to - %s', + os.path.abspath(download_file.name)) response = __get_url_response(url) download_file.write(response.read()) return download_file @@ -74,6 +78,76 @@ def download(url, dest_path, name=None): download_file.close() +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 generated by cryptography + :param pub_file_path: the path to the public keys + :param priv_file_path: the path to the private keys + """ + if keys: + if pub_file_path: + # To support '~' + pub_expand_file = os.path.expanduser(pub_file_path) + pub_dir = os.path.dirname(pub_expand_file) + + if not os.path.isdir(pub_dir): + os.mkdir(pub_dir) + + public_handle = None + try: + public_handle = open(pub_expand_file, 'wb') + public_bytes = keys.public_key().public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.OpenSSH) + public_handle.write(public_bytes) + finally: + if public_handle: + public_handle.close() + + os.chmod(pub_expand_file, 0o400) + logger.info("Saved public key to - " + pub_expand_file) + if priv_file_path: + # To support '~' + priv_expand_file = os.path.expanduser(priv_file_path) + priv_dir = os.path.dirname(priv_expand_file) + if not os.path.isdir(priv_dir): + os.mkdir(priv_dir) + + private_handle = None + try: + private_handle = open(priv_expand_file, 'wb') + private_handle.write( + keys.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption())) + finally: + if private_handle: + private_handle.close() + + os.chmod(priv_expand_file, 0o400) + logger.info("Saved private key to - " + priv_expand_file) + + +def save_string_to_file(string, file_path, mode=None): + """ + Stores + :param string: the string contents to store + :param file_path: the file path to create + :param mode: the file's mode + :return: the file object + """ + save_file = open(file_path, 'w') + try: + save_file.write(string) + if mode: + os.chmod(file_path, mode) + return save_file + finally: + save_file.close() + + def get_content_length(url): """ Returns the number of bytes to be downloaded from the given URL diff --git a/snaps/openstack/create_instance.py b/snaps/openstack/create_instance.py index b09e879..6b9a122 100644 --- a/snaps/openstack/create_instance.py +++ b/snaps/openstack/create_instance.py @@ -68,7 +68,11 @@ class OpenStackVmInstance: def create(self, cleanup=False, block=False): """ Creates a VM instance - :param cleanup: When true, only perform lookups for OpenStack objects. + :param cleanup: When true, this object is initialized only via queries, + else objects will be created when the queries return + None. The name of this parameter should be changed to + something like 'readonly' as the same goes with all of + the other creator classes. :param block: Thread will block until instance has either become active, error, or timeout waiting. Additionally, when True, floating IPs will not be applied @@ -102,11 +106,16 @@ class OpenStackVmInstance: fips = neutron_utils.get_floating_ips(self.__neutron, self.__ports) - for port_name, fip in fips: + for port_id, fip in fips: settings = self.instance_settings.floating_ip_settings for fip_setting in settings: - if port_name == fip_setting.port_name: + if port_id == fip_setting.port_id: self.__floating_ip_dict[fip_setting.name] = fip + else: + port = neutron_utils.get_port_by_id( + self.__neutron, port_id) + if port and port.name == fip_setting.port_name: + self.__floating_ip_dict[fip_setting.name] = fip def __create_vm(self, block=False): """ @@ -213,7 +222,7 @@ class OpenStackVmInstance: # Cleanup ports for name, port in self.__ports: - logger.info('Deleting Port - ' + name) + logger.info('Deleting Port with ID - %S ' + port.id) try: neutron_utils.delete_port(self.__neutron, port) except PortNotFoundClient as e: @@ -263,6 +272,14 @@ class OpenStackVmInstance: for port_setting in port_settings: port = neutron_utils.get_port( self.__neutron, port_settings=port_setting) + if not port: + network = neutron_utils.get_network( + self.__neutron, network_name=port_setting.network_name) + net_ports = neutron_utils.get_ports(self.__neutron, network) + for net_port in net_ports: + if port_setting.mac_address == net_port.mac_address: + port = net_port + break if port: ports.append((port_setting.name, port)) elif not cleanup: @@ -331,7 +348,7 @@ class OpenStackVmInstance: Returns the latest version of this server object from OpenStack :return: Server object """ - return self.__vm + return nova_utils.get_server_object_by_id(self.__nova, self.__vm.id) def get_console_output(self): """ @@ -506,8 +523,8 @@ class OpenStackVmInstance: def vm_active(self, block=False, poll_interval=POLL_INTERVAL): """ - Returns true when the VM status returns the value of - expected_status_code + Returns true when the VM status returns the value of the constant + STATUS_ACTIVE :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 @@ -560,7 +577,10 @@ class OpenStackVmInstance: :return: T/F """ if not self.__vm: - return False + if expected_status_code == STATUS_DELETED: + return True + else: + return False status = nova_utils.get_server_status(self.__nova, self.__vm) if not status: @@ -702,7 +722,7 @@ class VmInstanceSettings: """ Constructor :param name: the name of the VM - :param flavor: the VM's flavor + :param flavor: the VM's flavor name :param port_settings: the port configuration settings (required) :param security_group_names: a set of names of the security groups to add to the VM @@ -816,6 +836,7 @@ class FloatingIpSettings: """ self.name = kwargs.get('name') self.port_name = kwargs.get('port_name') + self.port_id = kwargs.get('port_id') self.router_name = kwargs.get('router_name') self.subnet_name = kwargs.get('subnet_name') if kwargs.get('provisioning') is not None: @@ -823,10 +844,14 @@ class FloatingIpSettings: else: self.provisioning = True - if not self.name or not self.port_name or not self.router_name: + # if not self.name or not self.port_name or not self.router_name: + if not self.name or not self.router_name: + raise FloatingIpSettingsError( + 'The attributes name, port_name and router_name are required') + + if not self.port_name and not self.port_id: raise FloatingIpSettingsError( - 'The attributes name, port_name and router_name are required ' - 'for FloatingIPSettings') + 'The attributes port_name or port_id are required') class VmInstanceSettingsError(Exception): diff --git a/snaps/openstack/create_keypairs.py b/snaps/openstack/create_keypairs.py index c81fef5..cc32da3 100644 --- a/snaps/openstack/create_keypairs.py +++ b/snaps/openstack/create_keypairs.py @@ -78,7 +78,7 @@ class OpenStackKeypair: self.__keypair = nova_utils.upload_keypair( self.__nova, self.keypair_settings.name, nova_utils.public_key_openssh(keys)) - nova_utils.save_keys_to_files( + file_utils.save_keys_to_files( keys, self.keypair_settings.public_filepath, self.keypair_settings.private_filepath) diff --git a/snaps/openstack/create_network.py b/snaps/openstack/create_network.py index d0b6d20..166a682 100644 --- a/snaps/openstack/create_network.py +++ b/snaps/openstack/create_network.py @@ -394,10 +394,10 @@ class PortSettings: def __init__(self, **kwargs): """ - Constructor - all parameters are optional - :param name: A symbolic name for the port. + Constructor + :param name: A symbolic name for the port (optional). :param network_name: The name of the network on which to create the - port. + port (required). :param admin_state_up: A boolean value denoting the administrative status of the port. True = up / False = down :param project_name: The name of the project who owns the network. @@ -453,10 +453,9 @@ class PortSettings: self.device_owner = kwargs.get('device_owner') self.device_id = kwargs.get('device_id') - if not self.name or not self.network_name: + if not self.network_name: raise PortSettingsError( - 'The attributes neutron, name, and network_name are required ' - 'for PortSettings') + 'The attribute network_name is required') def __set_fixed_ips(self, neutron): """ diff --git a/snaps/openstack/create_stack.py b/snaps/openstack/create_stack.py index 454cb18..ffe87a5 100644 --- a/snaps/openstack/create_stack.py +++ b/snaps/openstack/create_stack.py @@ -18,8 +18,10 @@ import time from heatclient.exc import HTTPNotFound -from snaps.openstack.create_network import ( - OpenStackNetwork, NetworkSettings, SubnetSettings) +from snaps.openstack.create_instance import OpenStackVmInstance +from snaps.openstack.utils import nova_utils, settings_utils, glance_utils + +from snaps.openstack.create_network import OpenStackNetwork from snaps.openstack.utils import heat_utils, neutron_utils __author__ = 'spisarski' @@ -31,6 +33,7 @@ POLL_INTERVAL = 3 STATUS_CREATE_FAILED = 'CREATE_FAILED' STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE' STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE' +STATUS_DELETE_FAILED = 'DELETE_FAILED' class OpenStackHeatStack: @@ -38,15 +41,33 @@ class OpenStackHeatStack: Class responsible for creating an heat stack in OpenStack """ - def __init__(self, os_creds, stack_settings): + def __init__(self, os_creds, stack_settings, image_settings=None, + keypair_settings=None): """ Constructor :param os_creds: The OpenStack connection credentials :param stack_settings: The stack settings + :param image_settings: A list of ImageSettings objects that were used + for spawning this stack + :param image_settings: A list of ImageSettings objects that were used + for spawning this stack + :param keypair_settings: A list of KeypairSettings objects that were + used for spawning this stack :return: """ self.__os_creds = os_creds self.stack_settings = stack_settings + + if image_settings: + self.image_settings = image_settings + else: + self.image_settings = None + + if image_settings: + self.keypair_settings = keypair_settings + else: + self.keypair_settings = None + self.__stack = None self.__heat_cli = None @@ -93,11 +114,39 @@ class OpenStackHeatStack: """ if self.__stack: try: + logger.info('Deleting stack - %s' + self.__stack.name) + heat_utils.delete_stack(self.__heat_cli, self.__stack) + + try: + self.stack_deleted(block=True) + except StackError as e: + # Stack deletion seems to fail quite a bit + logger.warn('Stack did not delete properly - %s', e) + + # Delete VMs first + for vm_inst_creator in self.get_vm_inst_creators(): + try: + vm_inst_creator.clean() + if not vm_inst_creator.vm_deleted(block=True): + logger.warn('Unable to deleted VM - %s', + vm_inst_creator.get_vm_inst().name) + except: + logger.warn('Unexpected error deleting VM - %s ', + vm_inst_creator.get_vm_inst().name) + + logger.info('Attempting to delete again stack - %s', + self.__stack.name) + + # Delete Stack again heat_utils.delete_stack(self.__heat_cli, self.__stack) + deleted = self.stack_deleted(block=True) + if not deleted: + raise StackError( + 'Stack could not be deleted ' + self.__stack.name) except HTTPNotFound: pass - self.__stack = None + self.__stack = None def get_stack(self): """ @@ -113,7 +162,7 @@ class OpenStackHeatStack: object :return: """ - return heat_utils.get_stack_outputs(self.__heat_cli, self.__stack.id) + return heat_utils.get_outputs(self.__heat_cli, self.__stack) def get_status(self): """ @@ -137,7 +186,23 @@ class OpenStackHeatStack: if not timeout: timeout = self.stack_settings.stack_create_timeout return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout, - poll_interval) + poll_interval, STATUS_CREATE_FAILED) + + def stack_deleted(self, block=False, timeout=None, + poll_interval=POLL_INTERVAL): + """ + Returns true when the stack 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 + """ + if not timeout: + timeout = self.stack_settings.stack_create_timeout + return self._stack_status_check(STATUS_DELETE_COMPLETE, block, timeout, + poll_interval, STATUS_DELETE_FAILED) def get_network_creators(self): """ @@ -153,7 +218,7 @@ class OpenStackHeatStack: self.__heat_cli, neutron, self.__stack) for stack_network in stack_networks: - net_settings = self.__create_network_settings( + net_settings = settings_utils.create_network_settings( neutron, stack_network) net_creator = OpenStackNetwork(self.__os_creds, net_settings) out.append(net_creator) @@ -161,45 +226,41 @@ class OpenStackHeatStack: return out - def __create_network_settings(self, neutron, network): + def get_vm_inst_creators(self, heat_keypair_option=None): """ - Returns a NetworkSettings object - :param neutron: the neutron client - :param network: a SNAPS-OO Network domain object - :return: + Returns a list of VM Instance creator objects as configured by the heat + template + :return: list() of OpenStackVmInstance objects """ - return NetworkSettings( - name=network.name, network_type=network.type, - subnet_settings=self.__create_subnet_settings(neutron, network)) - def __create_subnet_settings(self, neutron, network): - """ - Returns a list of SubnetSettings objects for a given network - :param neutron: the OpenStack neutron client - :param network: the SNAPS-OO Network domain object - :return: a list - """ out = list() + nova = nova_utils.nova_client(self.__os_creds) + + stack_servers = heat_utils.get_stack_servers( + self.__heat_cli, nova, self.__stack) + + neutron = neutron_utils.neutron_client(self.__os_creds) + glance = glance_utils.glance_client(self.__os_creds) + + for stack_server in stack_servers: + vm_inst_settings = settings_utils.create_vm_inst_settings( + nova, neutron, stack_server) + image_settings = settings_utils.determine_image_settings( + glance, stack_server, self.image_settings) + keypair_settings = settings_utils.determine_keypair_settings( + self.__heat_cli, self.__stack, stack_server, + keypair_settings=self.keypair_settings, + priv_key_key=heat_keypair_option) + vm_inst_creator = OpenStackVmInstance( + self.__os_creds, vm_inst_settings, image_settings, + keypair_settings) + out.append(vm_inst_creator) + vm_inst_creator.create(cleanup=True) - subnets = neutron_utils.get_subnets_by_network(neutron, network) - for subnet in subnets: - kwargs = dict() - kwargs['cidr'] = subnet.cidr - kwargs['ip_version'] = subnet.ip_version - kwargs['name'] = subnet.name - kwargs['start'] = subnet.start - kwargs['end'] = subnet.end - kwargs['gateway_ip'] = subnet.gateway_ip - kwargs['enable_dhcp'] = subnet.enable_dhcp - kwargs['dns_nameservers'] = subnet.dns_nameservers - kwargs['host_routes'] = subnet.host_routes - kwargs['ipv6_ra_mode'] = subnet.ipv6_ra_mode - kwargs['ipv6_address_mode'] = subnet.ipv6_address_mode - out.append(SubnetSettings(**kwargs)) return out def _stack_status_check(self, expected_status_code, block, timeout, - poll_interval): + poll_interval, fail_status): """ Returns true when the stack status returns the value of expected_status_code @@ -209,6 +270,7 @@ class OpenStackHeatStack: value in seconds has been exceeded (False) :param timeout: The timeout value :param poll_interval: The polling interval in seconds + :param fail_status: Returns false if the fail_status code is found :return: T/F """ # sleep and wait for stack status change @@ -218,7 +280,7 @@ class OpenStackHeatStack: start = time.time() - timeout while timeout > time.time() - start: - status = self._status(expected_status_code) + status = self._status(expected_status_code, fail_status) if status: logger.debug( 'Stack is active with name - ' + self.stack_settings.name) @@ -234,7 +296,7 @@ class OpenStackHeatStack: 'Timeout checking for stack status for ' + expected_status_code) return False - def _status(self, expected_status_code): + def _status(self, expected_status_code, fail_status=STATUS_CREATE_FAILED): """ Returns True when active else False :param expected_status_code: stack status evaluated with this string @@ -247,8 +309,8 @@ class OpenStackHeatStack: 'Cannot stack status for stack with ID - ' + self.__stack.id) return False - if status == STATUS_CREATE_FAILED: - raise StackCreationError('Stack had an error during deployment') + if fail_status and status == fail_status: + raise StackError('Stack had an error') logger.debug('Stack status is - ' + status) return status == expected_status_code @@ -299,3 +361,9 @@ class StackCreationError(Exception): """ Exception to be thrown when an stack cannot be created """ + + +class StackError(Exception): + """ + General exception + """ diff --git a/snaps/openstack/tests/create_instance_tests.py b/snaps/openstack/tests/create_instance_tests.py index 19173d2..9c872bc 100644 --- a/snaps/openstack/tests/create_instance_tests.py +++ b/snaps/openstack/tests/create_instance_tests.py @@ -210,11 +210,22 @@ class FloatingIpSettingsUnitTests(unittest.TestCase): with self.assertRaises(FloatingIpSettingsError): FloatingIpSettings(**{'name': 'foo', 'router_name': 'bar'}) - def test_name_port_router_only(self): + def test_name_port_router_name_only(self): settings = FloatingIpSettings(name='foo', port_name='foo-port', router_name='bar-router') self.assertEqual('foo', settings.name) self.assertEqual('foo-port', settings.port_name) + self.assertIsNone(settings.port_id) + self.assertEqual('bar-router', settings.router_name) + self.assertIsNone(settings.subnet_name) + self.assertTrue(settings.provisioning) + + def test_name_port_router_id_only(self): + settings = FloatingIpSettings(name='foo', port_id='foo-port', + router_name='bar-router') + self.assertEqual('foo', settings.name) + self.assertEqual('foo-port', settings.port_id) + self.assertIsNone(settings.port_name) self.assertEqual('bar-router', settings.router_name) self.assertIsNone(settings.subnet_name) self.assertTrue(settings.provisioning) @@ -225,6 +236,7 @@ class FloatingIpSettingsUnitTests(unittest.TestCase): 'router_name': 'bar-router'}) self.assertEqual('foo', settings.name) self.assertEqual('foo-port', settings.port_name) + self.assertIsNone(settings.port_id) self.assertEqual('bar-router', settings.router_name) self.assertIsNone(settings.subnet_name) self.assertTrue(settings.provisioning) @@ -236,6 +248,7 @@ class FloatingIpSettingsUnitTests(unittest.TestCase): provisioning=False) self.assertEqual('foo', settings.name) self.assertEqual('foo-port', settings.port_name) + self.assertIsNone(settings.port_id) self.assertEqual('bar-router', settings.router_name) self.assertEqual('bar-subnet', settings.subnet_name) self.assertFalse(settings.provisioning) @@ -247,6 +260,7 @@ class FloatingIpSettingsUnitTests(unittest.TestCase): 'provisioning': False}) self.assertEqual('foo', settings.name) self.assertEqual('foo-port', settings.port_name) + self.assertIsNone(settings.port_id) self.assertEqual('bar-router', settings.router_name) self.assertEqual('bar-subnet', settings.subnet_name) self.assertFalse(settings.provisioning) @@ -672,7 +686,7 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase): self.assertEqual(ip_1, inst_creator.get_port_ip(self.port_1_name)) self.assertTrue(inst_creator.vm_active(block=True)) - self.assertEqual(vm_inst, inst_creator.get_vm_inst()) + self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id) def test_ssh_client_fip_before_active(self): """ @@ -706,7 +720,7 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase): inst_creator.add_security_group( self.sec_grp_creator.get_security_group()) - self.assertEqual(vm_inst, inst_creator.get_vm_inst()) + self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id) self.assertTrue(validate_ssh_client(inst_creator)) @@ -744,7 +758,7 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase): inst_creator.add_security_group( self.sec_grp_creator.get_security_group()) - self.assertEqual(vm_inst, inst_creator.get_vm_inst()) + self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id) self.assertTrue(validate_ssh_client(inst_creator)) @@ -782,7 +796,7 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase): inst_creator.add_security_group( self.sec_grp_creator.get_security_group()) - self.assertEqual(vm_inst, inst_creator.get_vm_inst()) + self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id) self.assertTrue(validate_ssh_client(inst_creator)) @@ -1434,7 +1448,7 @@ class CreateInstancePubPrivNetTests(OSIntegrationTestCase): vm_inst = self.inst_creator.create(block=True) - self.assertEqual(vm_inst, self.inst_creator.get_vm_inst()) + self.assertEqual(vm_inst.id, self.inst_creator.get_vm_inst().id) # Effectively blocks until VM has been properly activated self.assertTrue(self.inst_creator.vm_active(block=True)) diff --git a/snaps/openstack/tests/create_keypairs_tests.py b/snaps/openstack/tests/create_keypairs_tests.py index 7b75d05..d2de6fe 100644 --- a/snaps/openstack/tests/create_keypairs_tests.py +++ b/snaps/openstack/tests/create_keypairs_tests.py @@ -332,7 +332,7 @@ class CreateKeypairsTests(OSIntegrationTestCase): :return: """ keys = nova_utils.create_keys() - nova_utils.save_keys_to_files(keys=keys, + file_utils.save_keys_to_files(keys=keys, pub_file_path=self.pub_file_path) self.keypair_creator = OpenStackKeypair( self.os_creds, KeypairSettings(name=self.keypair_name, @@ -448,7 +448,7 @@ class CreateKeypairsCleanupTests(OSIntegrationTestCase): :return: """ keys = nova_utils.create_keys() - nova_utils.save_keys_to_files( + file_utils.save_keys_to_files( keys=keys, pub_file_path=self.pub_file_path, priv_file_path=self.priv_file_path) self.keypair_creator = OpenStackKeypair( @@ -468,7 +468,7 @@ class CreateKeypairsCleanupTests(OSIntegrationTestCase): :return: """ keys = nova_utils.create_keys() - nova_utils.save_keys_to_files( + file_utils.save_keys_to_files( keys=keys, pub_file_path=self.pub_file_path, priv_file_path=self.priv_file_path) self.keypair_creator = OpenStackKeypair( diff --git a/snaps/openstack/tests/create_stack_tests.py b/snaps/openstack/tests/create_stack_tests.py index 967e803..d2b138e 100644 --- a/snaps/openstack/tests/create_stack_tests.py +++ b/snaps/openstack/tests/create_stack_tests.py @@ -31,9 +31,9 @@ import uuid from snaps.openstack import create_stack from snaps.openstack.create_stack import StackSettings, StackSettingsError -from snaps.openstack.tests import openstack_tests +from snaps.openstack.tests import openstack_tests, create_instance_tests from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase -from snaps.openstack.utils import heat_utils, neutron_utils +from snaps.openstack.utils import heat_utils, neutron_utils, nova_utils __author__ = 'spisarski' @@ -122,7 +122,7 @@ class StackSettingsUnitTests(unittest.TestCase): class CreateStackSuccessTests(OSIntegrationTestCase): """ - Test for the CreateStack class defined in create_stack.py + Tests for the CreateStack class defined in create_stack.py """ def setUp(self): @@ -155,11 +155,14 @@ class CreateStackSuccessTests(OSIntegrationTestCase): self.network_name = self.guid + '-net' self.subnet_name = self.guid + '-subnet' + self.vm_inst_name = self.guid + '-inst' + self.env_values = { 'image_name': self.image_creator.image_settings.name, 'flavor_name': self.flavor_creator.flavor_settings.name, 'net_name': self.network_name, - 'subnet_name': self.subnet_name} + 'subnet_name': self.subnet_name, + 'inst_name': self.vm_inst_name} self.heat_tmplt_path = pkg_resources.resource_filename( 'snaps.openstack.tests.heat', 'test_heat_template.yaml') @@ -209,13 +212,7 @@ class CreateStackSuccessTests(OSIntegrationTestCase): self.assertIsNotNone(retrieved_stack) self.assertEqual(created_stack.name, retrieved_stack.name) self.assertEqual(created_stack.id, retrieved_stack.id) - self.assertIsNotNone(self.stack_creator.get_outputs()) - self.assertEquals(0, len(self.stack_creator.get_outputs())) - - resources = heat_utils.get_resources( - self.heat_cli, self.stack_creator.get_stack()) - self.assertIsNotNone(resources) - self.assertEqual(4, len(resources)) + self.assertEqual(0, len(self.stack_creator.get_outputs())) def test_create_stack_template_dict(self): """ @@ -240,8 +237,7 @@ class CreateStackSuccessTests(OSIntegrationTestCase): self.assertIsNotNone(retrieved_stack) self.assertEqual(created_stack.name, retrieved_stack.name) self.assertEqual(created_stack.id, retrieved_stack.id) - self.assertIsNotNone(self.stack_creator.get_outputs()) - self.assertEquals(0, len(self.stack_creator.get_outputs())) + self.assertEqual(0, len(self.stack_creator.get_outputs())) def test_create_delete_stack(self): """ @@ -265,8 +261,7 @@ class CreateStackSuccessTests(OSIntegrationTestCase): self.assertIsNotNone(retrieved_stack) self.assertEqual(created_stack.name, retrieved_stack.name) self.assertEqual(created_stack.id, retrieved_stack.id) - self.assertIsNotNone(self.stack_creator.get_outputs()) - self.assertEquals(0, len(self.stack_creator.get_outputs())) + self.assertEqual(0, len(self.stack_creator.get_outputs())) self.assertEqual(create_stack.STATUS_CREATE_COMPLETE, self.stack_creator.get_status()) @@ -309,7 +304,6 @@ class CreateStackSuccessTests(OSIntegrationTestCase): self.assertIsNotNone(retrieved_stack) self.assertEqual(created_stack1.name, retrieved_stack.name) self.assertEqual(created_stack1.id, retrieved_stack.id) - self.assertIsNotNone(self.stack_creator.get_outputs()) self.assertEqual(0, len(self.stack_creator.get_outputs())) # Should be retrieving the instance data @@ -354,6 +348,129 @@ class CreateStackSuccessTests(OSIntegrationTestCase): self.assertIsNotNone(subnet_by_id) self.assertEqual(subnet_by_name, subnet_by_id) + def test_retrieve_vm_inst_creators(self): + """ + Tests the creation of an OpenStack stack from Heat template file and + the retrieval of the network creator. + """ + stack_settings = StackSettings( + name=self.__class__.__name__ + '-' + str(self.guid) + '-stack', + template_path=self.heat_tmplt_path, + env_values=self.env_values) + self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds, + stack_settings) + created_stack = self.stack_creator.create() + self.assertIsNotNone(created_stack) + + vm_inst_creators = self.stack_creator.get_vm_inst_creators() + self.assertIsNotNone(vm_inst_creators) + self.assertEqual(1, len(vm_inst_creators)) + self.assertEqual(self.vm_inst_name, + vm_inst_creators[0].get_vm_inst().name) + + nova = nova_utils.nova_client(self.admin_os_creds) + vm_inst_by_name = nova_utils.get_server( + nova, server_name=vm_inst_creators[0].get_vm_inst().name) + self.assertEqual(vm_inst_creators[0].get_vm_inst(), vm_inst_by_name) + self.assertIsNotNone(nova_utils.get_server_object_by_id( + nova, vm_inst_creators[0].get_vm_inst().id)) + + +class CreateComplexStackTests(OSIntegrationTestCase): + """ + Tests for the CreateStack class defined in create_stack.py + """ + + def setUp(self): + """ + Instantiates the CreateStack object that is responsible for downloading + and creating an OS stack file within OpenStack + """ + super(self.__class__, self).__start__() + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + + self.heat_creds = self.admin_os_creds + self.heat_creds.project_name = self.admin_os_creds.project_name + + self.heat_cli = heat_utils.heat_client(self.heat_creds) + self.stack_creator = None + + self.image_creator = OpenStackImage( + self.heat_creds, openstack_tests.cirros_image_settings( + name=self.guid + '-image', + image_metadata=self.image_metadata)) + self.image_creator.create() + + self.network_name = self.guid + '-net' + self.subnet_name = self.guid + '-subnet' + self.flavor1_name = self.guid + '-flavor1' + self.flavor2_name = self.guid + '-flavor2' + self.vm_inst1_name = self.guid + '-inst1' + self.vm_inst2_name = self.guid + '-inst2' + self.keypair_name = self.guid + '-kp' + + self.env_values = { + 'image1_name': self.image_creator.image_settings.name, + 'image2_name': self.image_creator.image_settings.name, + 'flavor1_name': self.flavor1_name, + 'flavor2_name': self.flavor2_name, + 'net_name': self.network_name, + 'subnet_name': self.subnet_name, + 'inst1_name': self.vm_inst1_name, + 'inst2_name': self.vm_inst2_name, + 'keypair_name': self.keypair_name} + + self.heat_tmplt_path = pkg_resources.resource_filename( + 'snaps.openstack.tests.heat', 'floating_ip_heat_template.yaml') + + def tearDown(self): + """ + Cleans the stack and downloaded stack file + """ + if self.stack_creator: + try: + self.stack_creator.clean() + except: + pass + + if self.image_creator: + try: + self.image_creator.clean() + except: + pass + + super(self.__class__, self).__clean__() + + def test_connect_via_ssh_heat_vm(self): + """ + Tests the creation of an OpenStack stack from Heat template file and + the retrieval of two VM instance creators and attempt to connect via + SSH to the first one with a floating IP. + """ + stack_settings = StackSettings( + name=self.__class__.__name__ + '-' + str(self.guid) + '-stack', + template_path=self.heat_tmplt_path, + env_values=self.env_values) + self.stack_creator = create_stack.OpenStackHeatStack( + self.heat_creds, stack_settings, + [self.image_creator.image_settings]) + created_stack = self.stack_creator.create() + self.assertIsNotNone(created_stack) + + vm_inst_creators = self.stack_creator.get_vm_inst_creators( + heat_keypair_option='private_key') + self.assertIsNotNone(vm_inst_creators) + self.assertEqual(2, len(vm_inst_creators)) + + for vm_inst_creator in vm_inst_creators: + if vm_inst_creator.get_vm_inst().name == self.vm_inst1_name: + self.assertTrue( + create_instance_tests.validate_ssh_client(vm_inst_creator)) + else: + vm_settings = vm_inst_creator.instance_settings + self.assertEqual(0, len(vm_settings.floating_ip_settings)) + class CreateStackNegativeTests(OSIntegrationTestCase): """ diff --git a/snaps/openstack/tests/heat/floating_ip_heat_template.yaml b/snaps/openstack/tests/heat/floating_ip_heat_template.yaml new file mode 100644 index 0000000..9da1cb7 --- /dev/null +++ b/snaps/openstack/tests/heat/floating_ip_heat_template.yaml @@ -0,0 +1,161 @@ +############################################################################## +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################## +heat_template_version: 2015-04-30 + +description: > + Sample template with two VMs instantiated against different images and + flavors on the same network and the first one has a floating IP + +parameters: + image1_name: + type: string + label: Image ID for first VM + description: Image name to be used for first instance + default: image_1 + image2_name: + type: string + label: Image ID for second VM + description: Image name to be used for second instance + default: image_2 + flavor1_name: + type: string + label: Instance Flavor for first VM + description: Flavor name for the first instance + default: m1.small + flavor2_name: + type: string + label: Instance Flavor for second VM + description: Flavor name for the second instance + default: m1.med + net_name: + type: string + label: Test network name + description: The name of the stack's network + default: test_net + subnet_name: + type: string + label: Test subnet name + description: The name of the stack's subnet + default: test_subnet + router_name: + type: string + label: Test router name + description: The name of the stack's router + default: mgmt_router + keypair_name: + type: string + label: Keypair name + description: The name of the stack's keypair + default: keypair_name + inst1_name: + type: string + label: First VM name + description: The name of the first VM to be spawned + default: test_vm1 + inst2_name: + type: string + label: Second VM name + description: The name of the second VM to be spawned + default: test_vm2 + external_net_name: + type: string + description: Name of the external network which management network will connect to + default: external + +resources: + flavor1: + type: OS::Nova::Flavor + properties: + ram: 4096 + vcpus: 4 + disk: 4 + flavor2: + type: OS::Nova::Flavor + properties: + ram: 4096 + vcpus: 4 + disk: 4 + + network: + type: OS::Neutron::Net + properties: + name: { get_param: net_name } + + subnet: + type: OS::Neutron::Subnet + properties: + name: { get_param: subnet_name } + ip_version: 4 + cidr: 10.1.2.0/24 + network: { get_resource: network } + + management_router: + type: OS::Neutron::Router + properties: + name: { get_param: router_name } + external_gateway_info: + network: { get_param: external_net_name } + + management_router_interface: + type: OS::Neutron::RouterInterface + properties: + router: { get_resource: management_router } + subnet: { get_resource: subnet } + + floating_ip: + type: OS::Neutron::FloatingIP + properties: + floating_network: { get_param: external_net_name } + + floating_ip_association: + type: OS::Nova::FloatingIPAssociation + properties: + floating_ip: { get_resource: floating_ip } + server_id: {get_resource: vm1} + + keypair: + type: OS::Nova::KeyPair + properties: + name: { get_param: keypair_name } + save_private_key: True + + vm1: + type: OS::Nova::Server + depends_on: [subnet, keypair, flavor1] + properties: + name: { get_param: inst1_name } + image: { get_param: image1_name } + flavor: { get_resource: flavor1 } + key_name: {get_resource: keypair} + networks: + - network: { get_resource: network } + + vm2: + type: OS::Nova::Server + depends_on: [subnet, flavor2] + properties: + name: { get_param: inst2_name } + image: { get_param: image2_name } + flavor: { get_resource: flavor2 } + key_name: {get_resource: keypair} + networks: + - network: { get_resource: network } + +outputs: + private_key: + description: "SSH Private Key" + value: { get_attr: [ keypair, private_key ]} diff --git a/snaps/openstack/tests/heat/test_heat_template.yaml b/snaps/openstack/tests/heat/test_heat_template.yaml index ffb82d6..03a34d8 100644 --- a/snaps/openstack/tests/heat/test_heat_template.yaml +++ b/snaps/openstack/tests/heat/test_heat_template.yaml @@ -1,3 +1,19 @@ +############################################################################## +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################## heat_template_version: 2015-04-30 description: Simple template to deploy a single compute instance @@ -23,6 +39,11 @@ parameters: label: Test subnet name description: The name of the stack's subnet default: test_subnet + inst_name: + type: string + label: Test VM name + description: The name of the spawned vm + default: test_vm resources: private_net: @@ -47,6 +68,7 @@ resources: my_instance: type: OS::Nova::Server properties: + name: { get_param: inst_name } image: { get_param: image_name } flavor: { get_param: flavor_name } networks: diff --git a/snaps/openstack/utils/heat_utils.py b/snaps/openstack/utils/heat_utils.py index c2919cb..6910bfe 100644 --- a/snaps/openstack/utils/heat_utils.py +++ b/snaps/openstack/utils/heat_utils.py @@ -17,12 +17,13 @@ import logging import yaml from heatclient.client import Client from heatclient.common.template_format import yaml_loader +from novaclient.exceptions import NotFound from oslo_serialization import jsonutils from snaps import file_utils -from snaps.domain.stack import Stack, Resource +from snaps.domain.stack import Stack, Resource, Output -from snaps.openstack.utils import keystone_utils, neutron_utils +from snaps.openstack.utils import keystone_utils, neutron_utils, nova_utils __author__ = 'spisarski' @@ -86,17 +87,6 @@ def get_stack_status(heat_cli, stack_id): return heat_cli.stacks.get(stack_id).stack_status -def get_stack_outputs(heat_cli, stack_id): - """ - Returns a domain Stack object for a given ID - :param heat_cli: the OpenStack heat client - :param stack_id: the ID of the heat stack to retrieve - :return: the Stack domain object else None - """ - stack = heat_cli.stacks.get(stack_id) - return stack.outputs - - def create_stack(heat_cli, stack_settings): """ Executes an Ansible playbook to the given host @@ -157,6 +147,29 @@ def get_resources(heat_cli, stack): return out +def get_outputs(heat_cli, stack): + """ + Returns all of the SNAPS-OO Output domain objects for the defined outputs + for given stack + :param heat_cli: the OpenStack heat client + :param stack: the SNAPS-OO Stack domain object + :return: a list + """ + out = list() + + os_stack = heat_cli.stacks.get(stack.id) + + outputs = None + if os_stack: + outputs = os_stack.outputs + + if outputs: + for output in outputs: + out.append(Output(**output)) + + return out + + def get_stack_networks(heat_cli, neutron, stack): """ Returns an instance of NetworkSettings for each network owned by this stack @@ -178,6 +191,31 @@ def get_stack_networks(heat_cli, neutron, stack): return out +def get_stack_servers(heat_cli, nova, stack): + """ + Returns an instance of NetworkSettings for each network owned by this stack + :param heat_cli: the OpenStack heat client object + :param nova: the OpenStack nova client object + :param stack: the SNAPS-OO Stack domain object + :return: a list of NetworkSettings + """ + + out = list() + resources = get_resources(heat_cli, stack) + for resource in resources: + if resource.type == 'OS::Nova::Server': + try: + server = nova_utils.get_server_object_by_id( + nova, resource.id) + if server: + out.append(server) + except NotFound: + logger.warn( + 'VmInst cannot be located with ID %s', resource.id) + + return out + + def parse_heat_template_str(tmpl_str): """ Takes a heat template string, performs some simple validation and returns a diff --git a/snaps/openstack/utils/neutron_utils.py b/snaps/openstack/utils/neutron_utils.py index e21c905..806bb53 100644 --- a/snaps/openstack/utils/neutron_utils.py +++ b/snaps/openstack/utils/neutron_utils.py @@ -248,6 +248,18 @@ def delete_router(neutron, router): neutron.delete_router(router=router.id) +def get_router_by_id(neutron, router_id): + """ + Returns a router with a given ID, else None if not found + :param neutron: the client + :param router_id: the Router ID + :return: a SNAPS-OO Router domain object + """ + router = neutron.show_router(router_id) + if router: + return Router(**router['router']) + + def get_router(neutron, router_settings=None, router_name=None): """ Returns the first router object (dictionary) found the given the settings @@ -385,23 +397,64 @@ def get_port(neutron, port_settings=None, port_name=None): port_filter = dict() if port_settings: - port_filter['name'] = port_settings.name + if port_settings.name and len(port_settings.name) > 0: + port_filter['name'] = port_settings.name if port_settings.admin_state_up: port_filter['admin_state_up'] = port_settings.admin_state_up if port_settings.device_id: port_filter['device_id'] = port_settings.device_id if port_settings.mac_address: port_filter['mac_address'] = port_settings.mac_address + if port_settings.network_name: + network = get_network(neutron, + network_name=port_settings.network_name) + port_filter['network_id'] = network.id elif port_name: port_filter['name'] = port_name ports = neutron.list_ports(**port_filter) for port in ports['ports']: - return Port(name=port['name'], id=port['id'], - ips=port['fixed_ips'], mac_address=port['mac_address']) + return Port(**port) + return None + + +def get_port_by_id(neutron, port_id): + """ + Returns a SNAPS-OO Port domain object for the given ID or none if not found + :param neutron: the client + :param port_id: the to query + :return: a SNAPS-OO Port domain object or None + """ + port = neutron.show_port(port_id) + if port: + return Port(**port['port']) return None +def get_ports(neutron, network, ips=None): + """ + Returns a list of SNAPS-OO Port objects for all OpenStack Port objects that + are associated with the 'network' parameter + :param neutron: the client + :param network: SNAPS-OO Network domain object + :param ips: the IPs to lookup if not None + :return: a SNAPS-OO Port domain object or None if not found + """ + out = list() + ports = neutron.list_ports(**{'network_id': network.id}) + for port in ports['ports']: + if ips: + for fixed_ips in port['fixed_ips']: + if ('ip_address' in fixed_ips and + fixed_ips['ip_address'] in ips) or ips is None: + out.append(Port(**port)) + break + else: + out.append(Port(**port)) + + return out + + def create_security_group(neutron, keystone, sec_grp_settings): """ Creates a security group object in OpenStack @@ -554,12 +607,13 @@ def get_floating_ips(neutron, ports=None): Returns all of the floating IPs When ports is not None, FIPs returned must be associated with one of the ports in the list and a tuple 2 where the first element being the port's - name and the second being the FloatingIp SNAPS-OO domain object. + ID and the second being the FloatingIp SNAPS-OO domain object. When ports is None, all known FloatingIp SNAPS-OO domain objects will be returned in a list :param neutron: the Neutron client - :param ports: a list of SNAPS-OO Port objects to join - :return: a list of tuple 2 (port_name, SNAPS FloatingIp) objects when ports + :param ports: a list of tuple 2 where index 0 is the port name and index 1 + is the SNAPS-OO Port object + :return: a list of tuple 2 (port_id, SNAPS FloatingIp) objects when ports is not None else a list of Port objects """ out = list() @@ -567,13 +621,11 @@ def get_floating_ips(neutron, ports=None): for fip in fips['floatingips']: if ports: for port_name, port in ports: - if fip['port_id'] == port.id: - out.append((port.name, FloatingIp( - inst_id=fip['id'], ip=fip['floating_ip_address']))) + if port and port.id == fip['port_id']: + out.append((port.id, FloatingIp(**fip))) break else: - out.append(FloatingIp(inst_id=fip['id'], - ip=fip['floating_ip_address'])) + out.append(FloatingIp(**fip)) return out @@ -593,7 +645,7 @@ def create_floating_ip(neutron, ext_net_name): body={'floatingip': {'floating_network_id': ext_net.id}}) - return FloatingIp(inst_id=fip['floatingip']['id'], + return FloatingIp(id=fip['floatingip']['id'], ip=fip['floatingip']['floating_ip_address']) else: raise NeutronException( @@ -612,8 +664,7 @@ def get_floating_ip(neutron, floating_ip): floating_ip.ip) os_fip = __get_os_floating_ip(neutron, floating_ip) if os_fip: - return FloatingIp( - inst_id=os_fip['id'], ip=os_fip['floating_ip_address']) + return FloatingIp(id=os_fip['id'], ip=os_fip['floating_ip_address']) def __get_os_floating_ip(neutron, floating_ip): @@ -648,7 +699,7 @@ def delete_floating_ip(neutron, floating_ip): def get_network_quotas(neutron, project_id): """ Returns a list of all available keypairs - :param nova: the Nova client + :param neutron: the neutron client :param project_id: the project's ID of the quotas to lookup :return: an object of type NetworkQuotas or None if not found """ diff --git a/snaps/openstack/utils/nova_utils.py b/snaps/openstack/utils/nova_utils.py index 0a259b0..fe53211 100644 --- a/snaps/openstack/utils/nova_utils.py +++ b/snaps/openstack/utils/nova_utils.py @@ -99,8 +99,8 @@ def create_server(nova, neutron, glance, instance_settings, image_settings, args['availability_zone'] = instance_settings.availability_zone server = nova.servers.create(**args) - return VmInst(name=server.name, inst_id=server.id, - networks=server.networks) + + return __map_os_server_obj_to_vm_inst(server) else: raise NovaException( 'Cannot create instance, image cannot be located with name %s', @@ -125,8 +125,27 @@ def get_server(nova, vm_inst_settings=None, server_name=None): servers = nova.servers.list(search_opts=search_opts) for server in servers: - return VmInst(name=server.name, inst_id=server.id, - networks=server.networks) + return __map_os_server_obj_to_vm_inst(server) + + +def __map_os_server_obj_to_vm_inst(os_server): + """ + Returns a VmInst object for an OpenStack Server object + :param os_server: the OpenStack server object + :return: an equivalent SNAPS-OO VmInst domain object + """ + sec_grp_names = list() + # VM must be active for 'security_groups' attr to be initialized + if hasattr(os_server, 'security_groups'): + for sec_group in os_server.security_groups: + if sec_group.get('name'): + sec_grp_names.append(sec_group.get('name')) + + return VmInst( + name=os_server.name, inst_id=os_server.id, + image_id=os_server.image['id'], flavor_id=os_server.flavor['id'], + networks=os_server.networks, keypair_name=os_server.key_name, + sec_grp_names=sec_grp_names) def __get_latest_server_os_object(nova, server): @@ -136,7 +155,17 @@ def __get_latest_server_os_object(nova, server): :param server: the domain VmInst object :return: the list of servers or None if not found """ - return nova.servers.get(server.id) + return __get_latest_server_os_object_by_id(nova, server.id) + + +def __get_latest_server_os_object_by_id(nova, server_id): + """ + Returns a server with a given id + :param nova: the Nova client + :param server_id: the server's ID + :return: the list of servers or None if not found + """ + return nova.servers.get(server_id) def get_server_status(nova, server): @@ -173,8 +202,18 @@ def get_latest_server_object(nova, server): :return: the list of servers or None if not found """ server = __get_latest_server_os_object(nova, server) - return VmInst(name=server.name, inst_id=server.id, - networks=server.networks) + return __map_os_server_obj_to_vm_inst(server) + + +def get_server_object_by_id(nova, server_id): + """ + Returns a server with a given id + :param nova: the Nova client + :param server_id: the server's id + :return: an SNAPS-OO VmInst object or None if not found + """ + server = __get_latest_server_os_object_by_id(nova, server_id) + return __map_os_server_obj_to_vm_inst(server) def get_server_security_group_names(nova, server): @@ -225,58 +264,6 @@ def public_key_openssh(keys): serialization.PublicFormat.OpenSSH) -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 generated by cryptography - :param pub_file_path: the path to the public keys - :param priv_file_path: the path to the private keys - """ - if keys: - if pub_file_path: - # To support '~' - pub_expand_file = os.path.expanduser(pub_file_path) - pub_dir = os.path.dirname(pub_expand_file) - - if not os.path.isdir(pub_dir): - os.mkdir(pub_dir) - - public_handle = None - try: - public_handle = open(pub_expand_file, 'wb') - public_bytes = keys.public_key().public_bytes( - serialization.Encoding.OpenSSH, - serialization.PublicFormat.OpenSSH) - public_handle.write(public_bytes) - finally: - if public_handle: - public_handle.close() - - os.chmod(pub_expand_file, 0o400) - logger.info("Saved public key to - " + pub_expand_file) - if priv_file_path: - # To support '~' - priv_expand_file = os.path.expanduser(priv_file_path) - priv_dir = os.path.dirname(priv_expand_file) - if not os.path.isdir(priv_dir): - os.mkdir(priv_dir) - - private_handle = None - try: - private_handle = open(priv_expand_file, 'wb') - private_handle.write( - keys.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption())) - finally: - if private_handle: - private_handle.close() - - os.chmod(priv_expand_file, 0o400) - logger.info("Saved private key to - " + priv_expand_file) - - def upload_keypair_file(nova, name, file_path): """ Uploads a public key from a file @@ -305,7 +292,8 @@ def upload_keypair(nova, name, key): """ logger.info('Creating keypair with name - ' + name) os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8')) - return Keypair(name=os_kp.name, id=os_kp.id, public_key=os_kp.public_key) + return Keypair(name=os_kp.name, kp_id=os_kp.id, + public_key=os_kp.public_key, fingerprint=os_kp.fingerprint) def keypair_exists(nova, keypair_obj): @@ -317,7 +305,7 @@ def keypair_exists(nova, keypair_obj): """ try: os_kp = nova.keypairs.get(keypair_obj) - return Keypair(name=os_kp.name, id=os_kp.id, + return Keypair(name=os_kp.name, kp_id=os_kp.id, public_key=os_kp.public_key) except: return None @@ -334,7 +322,7 @@ def get_keypair_by_name(nova, name): for keypair in keypairs: if keypair.name == name: - return Keypair(name=keypair.name, id=keypair.id, + return Keypair(name=keypair.name, kp_id=keypair.id, public_key=keypair.public_key) return None @@ -377,15 +365,15 @@ def delete_vm_instance(nova, vm_inst): nova.servers.delete(vm_inst.id) -def __get_os_flavor(nova, flavor): +def __get_os_flavor(nova, flavor_id): """ Returns to OpenStack flavor object by name :param nova: the Nova client - :param flavor: the SNAPS flavor domain object + :param flavor_id: the flavor's ID value :return: the OpenStack Flavor object """ try: - return nova.flavors.get(flavor.id) + return nova.flavors.get(flavor_id) except NotFound: return None @@ -397,7 +385,7 @@ def get_flavor(nova, flavor): :param flavor: the SNAPS flavor domain object :return: the SNAPS Flavor domain object """ - os_flavor = __get_os_flavor(nova, flavor) + os_flavor = __get_os_flavor(nova, flavor.id) if os_flavor: return Flavor( name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram, @@ -410,6 +398,22 @@ def get_flavor(nova, flavor): return None +def get_flavor_by_id(nova, flavor_id): + """ + Returns to OpenStack flavor object by name + :param nova: the Nova client + :param flavor_id: the flavor ID value + :return: the SNAPS Flavor domain object + """ + os_flavor = __get_os_flavor(nova, flavor_id) + if os_flavor: + return Flavor( + name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram, + disk=os_flavor.disk, vcpus=os_flavor.vcpus, + ephemeral=os_flavor.ephemeral, swap=os_flavor.swap, + rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public) + + def __get_os_flavor_by_name(nova, name): """ Returns to OpenStack flavor object by name @@ -475,7 +479,7 @@ def set_flavor_keys(nova, flavor, metadata): :param flavor: the SNAPS flavor domain object :param metadata: the metadata to set """ - os_flavor = __get_os_flavor(nova, flavor) + os_flavor = __get_os_flavor(nova, flavor.id) if os_flavor: os_flavor.set_keys(metadata) @@ -486,7 +490,7 @@ def get_flavor_keys(nova, flavor): :param nova: the Nova client :param flavor: the SNAPS flavor domain object """ - os_flavor = __get_os_flavor(nova, flavor) + os_flavor = __get_os_flavor(nova, flavor.id) if os_flavor: return os_flavor.get_keys() diff --git a/snaps/openstack/utils/settings_utils.py b/snaps/openstack/utils/settings_utils.py new file mode 100644 index 0000000..7f00075 --- /dev/null +++ b/snaps/openstack/utils/settings_utils.py @@ -0,0 +1,219 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid + +from snaps import file_utils +from snaps.openstack.create_instance import ( + VmInstanceSettings, FloatingIpSettings) +from snaps.openstack.create_keypairs import KeypairSettings +from snaps.openstack.create_network import ( + PortSettings, SubnetSettings, NetworkSettings) +from snaps.openstack.utils import ( + neutron_utils, nova_utils, heat_utils, glance_utils) + + +def create_network_settings(neutron, network): + """ + Returns a NetworkSettings object + :param neutron: the neutron client + :param network: a SNAPS-OO Network domain object + :return: + """ + return NetworkSettings( + name=network.name, network_type=network.type, + subnet_settings=create_subnet_settings(neutron, network)) + + +def create_subnet_settings(neutron, network): + """ + Returns a list of SubnetSettings objects for a given network + :param neutron: the OpenStack neutron client + :param network: the SNAPS-OO Network domain object + :return: a list + """ + out = list() + + subnets = neutron_utils.get_subnets_by_network(neutron, network) + for subnet in subnets: + kwargs = dict() + kwargs['cidr'] = subnet.cidr + kwargs['ip_version'] = subnet.ip_version + kwargs['name'] = subnet.name + kwargs['start'] = subnet.start + kwargs['end'] = subnet.end + kwargs['gateway_ip'] = subnet.gateway_ip + kwargs['enable_dhcp'] = subnet.enable_dhcp + kwargs['dns_nameservers'] = subnet.dns_nameservers + kwargs['host_routes'] = subnet.host_routes + kwargs['ipv6_ra_mode'] = subnet.ipv6_ra_mode + kwargs['ipv6_address_mode'] = subnet.ipv6_address_mode + out.append(SubnetSettings(**kwargs)) + return out + + +def create_vm_inst_settings(nova, neutron, server): + """ + Returns a NetworkSettings object + :param nova: the nova client + :param neutron: the neutron client + :param server: a SNAPS-OO VmInst domain object + :return: + """ + + flavor_name = nova_utils.get_flavor_by_id(nova, server.flavor_id) + + kwargs = dict() + kwargs['name'] = server.name + kwargs['flavor'] = flavor_name + kwargs['port_settings'] = __create_port_settings( + neutron, server.networks) + kwargs['security_group_names'] = server.sec_grp_names + kwargs['floating_ip_settings'] = __create_floatingip_settings( + neutron, kwargs['port_settings']) + + return VmInstanceSettings(**kwargs) + + +def __create_port_settings(neutron, networks): + """ + Returns a list of port settings based on the networks parameter + :param neutron: the neutron client + :param networks: a dict where the key is the network name and the value + is a list of IP addresses + :return: + """ + out = list() + + for net_name, ips in networks.items(): + network = neutron_utils.get_network(neutron, network_name=net_name) + ports = neutron_utils.get_ports(neutron, network, ips) + for port in ports: + kwargs = dict() + if port.name: + kwargs['name'] = port.name + kwargs['network_name'] = network.name + kwargs['mac_address'] = port.mac_address + kwargs['allowed_address_pairs'] = port.allowed_address_pairs + kwargs['admin_state_up'] = port.admin_state_up + out.append(PortSettings(**kwargs)) + + return out + + +def __create_floatingip_settings(neutron, port_settings): + """ + Returns a list of FloatingIPSettings objects as they pertain to an + existing deployed server instance + :param neutron: the neutron client + :param port_settings: list of SNAPS-OO PortSettings objects + :return: a list of FloatingIPSettings objects or an empty list if no + floating IPs have been created + """ + base_fip_name = 'fip-' + fip_ctr = 1 + out = list() + + fip_ports = list() + for port_setting in port_settings: + setting_port = neutron_utils.get_port(neutron, port_setting) + if setting_port: + network = neutron_utils.get_network( + neutron, network_name=port_setting.network_name) + network_ports = neutron_utils.get_ports(neutron, network) + if network_ports: + for setting_port in network_ports: + if port_setting.mac_address == setting_port.mac_address: + fip_ports.append((port_setting.name, setting_port)) + break + + floating_ips = neutron_utils.get_floating_ips(neutron, fip_ports) + + for port_id, floating_ip in floating_ips: + router = neutron_utils.get_router_by_id(neutron, floating_ip.router_id) + setting_port = neutron_utils.get_port_by_id( + neutron, floating_ip.port_id) + kwargs = dict() + kwargs['name'] = base_fip_name + str(fip_ctr) + kwargs['port_name'] = setting_port.name + kwargs['port_id'] = setting_port.id + kwargs['router_name'] = router.name + + if setting_port: + for ip_dict in setting_port.ips: + if ('ip_address' in ip_dict and + 'subnet_id' in ip_dict and + ip_dict['ip_address'] == floating_ip.fixed_ip_address): + subnet = neutron_utils.get_subnet_by_id( + neutron, ip_dict['subnet_id']) + if subnet: + kwargs['subnet_name'] = subnet.name + + out.append(FloatingIpSettings(**kwargs)) + + fip_ctr += 1 + + return out + + +def determine_image_settings(glance, server, image_settings): + """ + Returns a ImageSettings object from the list that matches the name in one + of the image_settings parameter + :param glance: the glance client + :param server: a SNAPS-OO VmInst domain object + :param image_settings: list of ImageSettings objects + :return: ImageSettings or None + """ + if image_settings: + for image_setting in image_settings: + image = glance_utils.get_image_by_id(glance, server.image_id) + if image and image.name == image_setting.name: + return image_setting + + +def determine_keypair_settings(heat_cli, stack, server, keypair_settings=None, + priv_key_key=None): + """ + Returns a KeypairSettings object from the list that matches the + server.keypair_name value in the keypair_settings parameter if not None, + else if the output_key is not None, the output's value when contains the + string 'BEGIN RSA PRIVATE KEY', this value will be stored into a file and + encoded into the KeypairSettings object returned + :param heat_cli: the OpenStack heat client + :param stack: a SNAPS-OO Stack domain object + :param server: a SNAPS-OO VmInst domain object + :param keypair_settings: list of KeypairSettings objects + :param priv_key_key: the stack options that holds the private key value + :return: KeypairSettings or None + """ + # Existing keypair being used by Heat Template + if keypair_settings: + for keypair_setting in keypair_settings: + if server.keypair_name == keypair_setting.name: + return keypair_setting + + # Keypair created by Heat template + if priv_key_key: + outputs = heat_utils.get_outputs(heat_cli, stack) + for output in outputs: + if output.key == priv_key_key: + # Save to file + guid = uuid.uuid4() + key_file = file_utils.save_string_to_file( + output.value, str(guid), 0o400) + + # Use outputs, file and resources for the KeypairSettings + return KeypairSettings( + name=server.keypair_name, private_filepath=key_file.name) diff --git a/snaps/openstack/utils/tests/heat_utils_tests.py b/snaps/openstack/utils/tests/heat_utils_tests.py index 92432f6..a7dc2e2 100644 --- a/snaps/openstack/utils/tests/heat_utils_tests.py +++ b/snaps/openstack/utils/tests/heat_utils_tests.py @@ -22,10 +22,12 @@ from snaps.openstack import create_stack from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings from snaps.openstack.create_image import OpenStackImage +from snaps.openstack.create_instance import OpenStackVmInstance from snaps.openstack.create_stack import StackSettings from snaps.openstack.tests import openstack_tests from snaps.openstack.tests.os_source_file_test import OSComponentTestCase -from snaps.openstack.utils import heat_utils, neutron_utils +from snaps.openstack.utils import ( + heat_utils, neutron_utils, nova_utils, settings_utils, glance_utils) __author__ = 'spisarski' @@ -37,7 +39,7 @@ class HeatSmokeTests(OSComponentTestCase): Tests to ensure that the heat client can communicate with the cloud """ - def test_nova_connect_success(self): + def test_heat_connect_success(self): """ Tests to ensure that the proper credentials can connect. """ @@ -48,7 +50,7 @@ class HeatSmokeTests(OSComponentTestCase): for stack in stacks: print stack - def test_nova_connect_fail(self): + def test_heat_connect_fail(self): """ Tests to ensure that the improper credentials cannot connect. """ @@ -67,7 +69,7 @@ class HeatSmokeTests(OSComponentTestCase): print stack -class HeatUtilsCreateStackTests(OSComponentTestCase): +class HeatUtilsCreateSimpleStackTests(OSComponentTestCase): """ Test basic Heat functionality """ @@ -81,6 +83,7 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): stack_name2 = guid + '-stack2' self.network_name = guid + '-net' self.subnet_name = guid + '-subnet' + self.vm_inst_name = guid + '-inst' self.image_creator = OpenStackImage( self.os_creds, openstack_tests.cirros_image_settings( @@ -96,7 +99,8 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): env_values = {'image_name': self.image_creator.image_settings.name, 'flavor_name': self.flavor_creator.flavor_settings.name, 'net_name': self.network_name, - 'subnet_name': self.subnet_name} + 'subnet_name': self.subnet_name, + 'inst_name': self.vm_inst_name} heat_tmplt_path = pkg_resources.resource_filename( 'snaps.openstack.tests.heat', 'test_heat_template.yaml') self.stack_settings1 = StackSettings( @@ -156,13 +160,16 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): self.stack1.id) self.assertEqual(self.stack1, stack_query_3) - outputs = heat_utils.get_stack_outputs( - self.heat_client, self.stack1.id) + resources = heat_utils.get_resources(self.heat_client, self.stack1) + self.assertIsNotNone(resources) + self.assertEqual(4, len(resources)) + + outputs = heat_utils.get_outputs(self.heat_client, self.stack1) self.assertIsNotNone(outputs) self.assertEqual(0, len(outputs)) + # Wait until stack deployment has completed end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT - is_active = False while time.time() < end_time: status = heat_utils.get_stack_status(self.heat_client, @@ -178,10 +185,6 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): self.assertTrue(is_active) - resources = heat_utils.get_resources(self.heat_client, self.stack1) - self.assertIsNotNone(resources) - self.assertEqual(4, len(resources)) - neutron = neutron_utils.neutron_client(self.os_creds) networks = heat_utils.get_stack_networks( self.heat_client, neutron, self.stack1) @@ -193,6 +196,13 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): self.assertEqual(1, len(subnets)) self.assertEqual(self.subnet_name, subnets[0].name) + nova = nova_utils.nova_client(self.os_creds) + servers = heat_utils.get_stack_servers( + self.heat_client, nova, self.stack1) + self.assertIsNotNone(servers) + self.assertEqual(1, len(servers)) + self.assertEqual(self.vm_inst_name, servers[0].name) + def test_create_stack_x2(self): """ Tests the creation of an OpenStack keypair that does not exist. @@ -212,13 +222,7 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): self.stack1.id) self.assertEqual(self.stack1, stack1_query_3) - outputs = heat_utils.get_stack_outputs(self.heat_client, - self.stack1.id) - self.assertIsNotNone(outputs) - self.assertEqual(0, len(outputs)) - end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT - is_active = False while time.time() < end_time: status = heat_utils.get_stack_status(self.heat_client, @@ -249,11 +253,6 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): self.stack2.id) self.assertEqual(self.stack2, stack2_query_3) - outputs = heat_utils.get_stack_outputs(self.heat_client, - self.stack2.id) - self.assertIsNotNone(outputs) - self.assertEqual(0, len(outputs)) - end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT is_active = False @@ -270,3 +269,194 @@ class HeatUtilsCreateStackTests(OSComponentTestCase): time.sleep(3) self.assertTrue(is_active) + + +class HeatUtilsCreateComplexStackTests(OSComponentTestCase): + """ + Test basic Heat functionality + """ + + def setUp(self): + """ + Instantiates OpenStack instances that cannot be spawned by Heat + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + stack_name = guid + '-stack' + self.network_name = guid + '-net' + self.subnet_name = guid + '-subnet' + self.vm_inst1_name = guid + '-inst1' + self.vm_inst2_name = guid + '-inst2' + self.flavor1_name = guid + '-flavor1' + self.flavor2_name = guid + '-flavor2' + self.keypair_name = guid + '-keypair' + + self.image_creator1 = OpenStackImage( + self.os_creds, openstack_tests.cirros_image_settings( + name=guid + '-image1', image_metadata=self.image_metadata)) + self.image_creator1.create() + + self.image_creator2 = OpenStackImage( + self.os_creds, openstack_tests.cirros_image_settings( + name=guid + '-image2', image_metadata=self.image_metadata)) + self.image_creator2.create() + + env_values = {'image1_name': self.image_creator1.image_settings.name, + 'image2_name': self.image_creator2.image_settings.name, + 'flavor1_name': self.flavor1_name, + 'flavor2_name': self.flavor2_name, + 'net_name': self.network_name, + 'subnet_name': self.subnet_name, + 'keypair_name': self.keypair_name, + 'inst1_name': self.vm_inst1_name, + 'inst2_name': self.vm_inst2_name, + 'external_net_name': self.ext_net_name} + heat_tmplt_path = pkg_resources.resource_filename( + 'snaps.openstack.tests.heat', 'floating_ip_heat_template.yaml') + stack_settings = StackSettings( + name=stack_name, template_path=heat_tmplt_path, + env_values=env_values) + self.heat_client = heat_utils.heat_client(self.os_creds) + self.stack = heat_utils.create_stack(self.heat_client, stack_settings) + + # Wait until stack deployment has completed + end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT + is_active = False + while time.time() < end_time: + status = heat_utils.get_stack_status(self.heat_client, + self.stack.id) + if status == create_stack.STATUS_CREATE_COMPLETE: + is_active = True + break + elif status == create_stack.STATUS_CREATE_FAILED: + is_active = False + break + + time.sleep(3) + self.assertTrue(is_active) + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.stack: + try: + heat_utils.delete_stack(self.heat_client, self.stack) + # Wait until stack deployment has completed + end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT + is_deleted = False + while time.time() < end_time: + status = heat_utils.get_stack_status(self.heat_client, + self.stack.id) + if status == create_stack.STATUS_DELETE_COMPLETE: + is_deleted = True + break + elif status == create_stack.STATUS_DELETE_FAILED: + is_deleted = False + break + + time.sleep(3) + + if not is_deleted: + nova = nova_utils.nova_client(self.os_creds) + neutron = neutron_utils.neutron_client(self.os_creds) + glance = glance_utils.glance_client(self.os_creds) + servers = heat_utils.get_stack_servers( + self.heat_client, nova, self.stack) + for server in servers: + vm_settings = settings_utils.create_vm_inst_settings( + nova, neutron, server) + img_settings = settings_utils.determine_image_settings( + glance, server, + [self.image_creator1.image_settings, + self.image_creator2.image_settings]) + vm_creator = OpenStackVmInstance( + self.os_creds, vm_settings, img_settings) + vm_creator.create(cleanup=False) + vm_creator.clean() + vm_creator.vm_deleted(block=True) + + heat_utils.delete_stack(self.heat_client, self.stack) + time.sleep(20) + except: + raise + + if self.image_creator1: + try: + self.image_creator1.clean() + except: + pass + + if self.image_creator2: + try: + self.image_creator2.clean() + except: + pass + + def test_get_settings_from_stack(self): + """ + Tests that a heat template with floating IPs and can have the proper + settings derived from settings_utils.py. + """ + resources = heat_utils.get_resources(self.heat_client, self.stack) + self.assertIsNotNone(resources) + self.assertEqual(11, len(resources)) + + options = heat_utils.get_outputs(self.heat_client, self.stack) + self.assertIsNotNone(options) + self.assertEqual(1, len(options)) + + neutron = neutron_utils.neutron_client(self.os_creds) + networks = heat_utils.get_stack_networks( + self.heat_client, neutron, self.stack) + self.assertIsNotNone(networks) + self.assertEqual(1, len(networks)) + self.assertEqual(self.network_name, networks[0].name) + + network_settings = settings_utils.create_network_settings( + neutron, networks[0]) + self.assertIsNotNone(network_settings) + self.assertEqual(self.network_name, network_settings.name) + + nova = nova_utils.nova_client(self.os_creds) + glance = glance_utils.glance_client(self.os_creds) + + servers = heat_utils.get_stack_servers( + self.heat_client, nova, self.stack) + self.assertIsNotNone(servers) + self.assertEqual(2, len(servers)) + + image_settings = settings_utils.determine_image_settings( + glance, servers[0], + [self.image_creator1.image_settings, + self.image_creator2.image_settings]) + + self.assertIsNotNone(image_settings) + if image_settings.name.endswith('1'): + self.assertEqual( + self.image_creator1.image_settings.name, image_settings.name) + else: + self.assertEqual( + self.image_creator2.image_settings.name, image_settings.name) + + image_settings = settings_utils.determine_image_settings( + glance, servers[1], + [self.image_creator1.image_settings, + self.image_creator2.image_settings]) + if image_settings.name.endswith('1'): + self.assertEqual( + self.image_creator1.image_settings.name, image_settings.name) + else: + self.assertEqual( + self.image_creator2.image_settings.name, image_settings.name) + + keypair1_settings = settings_utils.determine_keypair_settings( + self.heat_client, self.stack, servers[0], + priv_key_key='private_key') + self.assertIsNotNone(keypair1_settings) + self.assertEqual(self.keypair_name, keypair1_settings.name) + + keypair2_settings = settings_utils.determine_keypair_settings( + self.heat_client, self.stack, servers[1], + priv_key_key='private_key') + self.assertIsNotNone(keypair2_settings) + self.assertEqual(self.keypair_name, keypair2_settings.name) diff --git a/snaps/openstack/utils/tests/neutron_utils_tests.py b/snaps/openstack/utils/tests/neutron_utils_tests.py index 5493f5b..05d508d 100644 --- a/snaps/openstack/utils/tests/neutron_utils_tests.py +++ b/snaps/openstack/utils/tests/neutron_utils_tests.py @@ -502,8 +502,7 @@ class NeutronUtilsRouterTests(OSComponentTestCase): def test_create_port_null_name(self): """ - Tests the neutron_utils.create_port() function for an Exception when - the port name value is None + Tests the neutron_utils.create_port() when the port name value is None """ self.network = neutron_utils.create_network( self.neutron, self.os_creds, self.net_config.network_settings) @@ -519,14 +518,16 @@ class NeutronUtilsRouterTests(OSComponentTestCase): self.assertTrue(validate_subnet( self.neutron, subnet_setting.name, subnet_setting.cidr, True)) - with self.assertRaises(Exception): - self.port = neutron_utils.create_port( - self.neutron, self.os_creds, - PortSettings( - network_name=self.net_config.network_settings.name, - ip_addrs=[{ - 'subnet_name': subnet_setting.name, - 'ip': ip_1}])) + self.port = neutron_utils.create_port( + self.neutron, self.os_creds, + PortSettings( + network_name=self.net_config.network_settings.name, + ip_addrs=[{ + 'subnet_name': subnet_setting.name, + 'ip': ip_1}])) + + port = neutron_utils.get_port_by_id(self.neutron, self.port.id) + self.assertEqual(self.port, port) def test_create_port_null_network_object(self): """ diff --git a/snaps/openstack/utils/tests/nova_utils_tests.py b/snaps/openstack/utils/tests/nova_utils_tests.py index b2eda97..c5b29b5 100644 --- a/snaps/openstack/utils/tests/nova_utils_tests.py +++ b/snaps/openstack/utils/tests/nova_utils_tests.py @@ -16,6 +16,10 @@ import logging import uuid import os +import time + +from snaps import file_utils +from snaps.openstack import create_instance from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor from snaps.openstack.create_image import OpenStackImage from snaps.openstack.create_instance import VmInstanceSettings @@ -130,7 +134,7 @@ class NovaUtilsKeypairTests(OSComponentTestCase): 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, + file_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, @@ -308,6 +312,19 @@ class NovaUtilsInstanceTests(OSComponentTestCase): self.assertIsNotNone(self.vm_inst) + # Wait until instance is ACTIVE + iters = 0 + active = False + while iters < 60: + if create_instance.STATUS_ACTIVE == nova_utils.get_server_status( + self.nova, self.vm_inst): + active = True + break + + time.sleep(3) + iters += 1 + + self.assertTrue(active) vm_inst = nova_utils.get_latest_server_object(self.nova, self.vm_inst) self.assertEqual(self.vm_inst.name, vm_inst.name) diff --git a/snaps/openstack/utils/tests/settings_utils_tests.py b/snaps/openstack/utils/tests/settings_utils_tests.py new file mode 100644 index 0000000..f84e6a0 --- /dev/null +++ b/snaps/openstack/utils/tests/settings_utils_tests.py @@ -0,0 +1,341 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os +import uuid + +from snaps.openstack import ( + create_image, create_network, create_router, create_flavor, + create_keypairs, create_instance) +from snaps.openstack.create_network import ( + NetworkSettings, OpenStackNetwork, SubnetSettings) +from snaps.openstack.create_security_group import ( + SecurityGroupRuleSettings, Direction, Protocol, OpenStackSecurityGroup, + SecurityGroupSettings) +from snaps.openstack.tests import openstack_tests +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import ( + neutron_utils, settings_utils, nova_utils, glance_utils) + +__author__ = 'spisarski' + +logger = logging.getLogger('nova_utils_tests') + + +class SettingsUtilsNetworkingTests(OSComponentTestCase): + """ + Tests the ability to reverse engineer NetworkSettings objects from existing + networks deployed to OpenStack + """ + + def setUp(self): + """ + Instantiates OpenStack instances that cannot be spawned by Heat + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.network_name = guid + '-net' + self.subnet_name = guid + '-subnet' + self.net_creator = None + self.neutron = neutron_utils.neutron_client(self.os_creds) + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.net_creator: + try: + self.net_creator.clean() + except: + pass + + def test_derive_net_settings_no_subnet(self): + """ + Validates the utility function settings_utils#create_network_settings + returns an acceptable NetworkSettings object and ensures that the + new settings object will not cause the new OpenStackNetwork instance + to create another network + """ + net_settings = NetworkSettings(name=self.network_name) + self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + network = self.net_creator.create() + + derived_settings = settings_utils.create_network_settings( + self.neutron, network) + + self.assertIsNotNone(derived_settings) + self.assertEqual(net_settings.name, derived_settings.name) + self.assertEqual(net_settings.admin_state_up, + derived_settings.admin_state_up) + self.assertEqual(net_settings.external, derived_settings.external) + self.assertEqual(len(net_settings.subnet_settings), + len(derived_settings.subnet_settings)) + + net_creator = OpenStackNetwork(self.os_creds, derived_settings) + derived_network = net_creator.create() + + self.assertEqual(network, derived_network) + + def test_derive_net_settings_two_subnets(self): + """ + Validates the utility function settings_utils#create_network_settings + returns an acceptable NetworkSettings object + """ + subnet_settings = list() + subnet_settings.append(SubnetSettings(name='sub1', cidr='10.0.0.0/24')) + subnet_settings.append(SubnetSettings(name='sub2', cidr='10.0.1.0/24')) + net_settings = NetworkSettings(name=self.network_name, + subnet_settings=subnet_settings) + self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + network = self.net_creator.create() + + derived_settings = settings_utils.create_network_settings( + self.neutron, network) + + self.assertIsNotNone(derived_settings) + self.assertEqual(net_settings.name, derived_settings.name) + self.assertEqual(net_settings.admin_state_up, + derived_settings.admin_state_up) + self.assertEqual(net_settings.external, derived_settings.external) + self.assertEqual(len(net_settings.subnet_settings), + len(derived_settings.subnet_settings)) + + # Validate the first subnet + orig_sub1 = net_settings.subnet_settings[0] + found = False + for derived_sub in derived_settings.subnet_settings: + if orig_sub1.name == derived_sub.name: + self.assertEqual(orig_sub1.cidr, derived_sub.cidr) + found = True + + self.assertTrue(found) + + # Validate the second subnet + orig_sub2 = net_settings.subnet_settings[1] + found = False + for derived_sub in derived_settings.subnet_settings: + if orig_sub2.name == derived_sub.name: + self.assertEqual(orig_sub2.cidr, derived_sub.cidr) + self.assertEqual(orig_sub2.ip_version, derived_sub.ip_version) + found = True + + self.assertTrue(found) + + +class SettingsUtilsVmInstTests(OSComponentTestCase): + """ + Tests the ability to reverse engineer VmInstanceSettings objects from + existing VMs/servers deployed to OpenStack + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading + and creating an OS image file within OpenStack + """ + # super(self.__class__, self).__start__() + + self.nova = nova_utils.nova_client(self.os_creds) + self.glance = glance_utils.glance_client(self.os_creds) + self.neutron = neutron_utils.neutron_client(self.os_creds) + + 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.sec_grp_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.cirros_image_settings( + name=guid + '-' + '-image', + image_metadata=self.image_metadata) + 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.os_creds, + create_flavor.FlavorSettings(name=guid + '-flavor-name', + ram=256, disk=1, vcpus=1)) + self.flavor_creator.create() + + # Create Key/Pair + self.keypair_creator = create_keypairs.OpenStackKeypair( + self.os_creds, create_keypairs.KeypairSettings( + name=self.keypair_name, + public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + + # Create Security Group + sec_grp_name = guid + '-sec-grp' + rule1 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name, + direction=Direction.ingress, + protocol=Protocol.icmp) + rule2 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name, + direction=Direction.ingress, + protocol=Protocol.tcp, + port_range_min=22, + port_range_max=22) + self.sec_grp_creator = OpenStackSecurityGroup( + self.os_creds, + SecurityGroupSettings(name=sec_grp_name, + rule_settings=[rule1, rule2])) + self.sec_grp_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: + self.tearDown() + raise + + def tearDown(self): + """ + Cleans the created objects + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except: + pass + + if self.sec_grp_creator: + try: + self.sec_grp_creator.clean() + except: + pass + + if self.keypair_creator: + try: + self.keypair_creator.clean() + except: + pass + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except: + pass + + if os.path.isfile(self.keypair_pub_filepath): + try: + os.remove(self.keypair_pub_filepath) + except: + pass + + if os.path.isfile(self.keypair_priv_filepath): + try: + os.remove(self.keypair_priv_filepath) + except: + pass + + if self.router_creator: + try: + self.router_creator.clean() + except: + pass + + if self.network_creator: + try: + self.network_creator.clean() + except: + pass + + if self.image_creator and not self.image_creator.image_settings.exists: + try: + self.image_creator.clean() + except: + pass + + if os.path.isfile(self.test_file_local_path): + os.remove(self.test_file_local_path) + + # super(self.__class__, self).__clean__() + + def test_derive_vm_inst_settings(self): + """ + Validates the utility function settings_utils#create_vm_inst_settings + returns an acceptable VmInstanceSettings object + """ + self.inst_creator.create(block=True) + + server = nova_utils.get_server( + self.nova, vm_inst_settings=self.inst_creator.instance_settings) + derived_vm_settings = settings_utils.create_vm_inst_settings( + self.nova, self.neutron, server) + self.assertIsNotNone(derived_vm_settings) + self.assertIsNotNone(derived_vm_settings.port_settings) + self.assertIsNotNone(derived_vm_settings.floating_ip_settings) + + def test_derive_image_settings(self): + """ + Validates the utility function settings_utils#create_image_settings + returns an acceptable ImageSettings object + """ + self.inst_creator.create(block=True) + + server = nova_utils.get_server( + self.nova, vm_inst_settings=self.inst_creator.instance_settings) + derived_image_settings = settings_utils.determine_image_settings( + self.glance, server, [self.image_creator.image_settings]) + self.assertIsNotNone(derived_image_settings) + self.assertEqual(self.image_creator.image_settings.name, + derived_image_settings.name) diff --git a/snaps/test_suite_builder.py b/snaps/test_suite_builder.py index e264b59..2162844 100644 --- a/snaps/test_suite_builder.py +++ b/snaps/test_suite_builder.py @@ -61,7 +61,8 @@ from snaps.openstack.tests.create_security_group_tests import ( CreateSecurityGroupTests, SecurityGroupRuleSettingsUnitTests, SecurityGroupSettingsUnitTests) from snaps.openstack.tests.create_stack_tests import ( - StackSettingsUnitTests, CreateStackSuccessTests, CreateStackNegativeTests) + StackSettingsUnitTests, CreateStackSuccessTests, CreateStackNegativeTests, + CreateComplexStackTests) from snaps.openstack.tests.create_user_tests import ( UserSettingsUnitTests, CreateUserSuccessTests) from snaps.openstack.tests.os_source_file_test import ( @@ -69,7 +70,8 @@ from snaps.openstack.tests.os_source_file_test import ( from snaps.openstack.utils.tests.glance_utils_tests import ( GlanceSmokeTests, GlanceUtilsTests) from snaps.openstack.utils.tests.heat_utils_tests import ( - HeatUtilsCreateStackTests, HeatSmokeTests) + HeatSmokeTests, HeatUtilsCreateSimpleStackTests, + HeatUtilsCreateComplexStackTests) from snaps.openstack.utils.tests.keystone_utils_tests import ( KeystoneSmokeTests, KeystoneUtilsTests) from snaps.openstack.utils.tests.neutron_utils_tests import ( @@ -273,7 +275,11 @@ def add_openstack_api_tests(suite, os_creds, ext_net_name, use_keystone=True, CreateFlavorTests, os_creds=os_creds, ext_net_name=ext_net_name, log_level=log_level)) suite.addTest(OSComponentTestCase.parameterize( - HeatUtilsCreateStackTests, os_creds=os_creds, + HeatUtilsCreateSimpleStackTests, os_creds=os_creds, + ext_net_name=ext_net_name, log_level=log_level, + image_metadata=image_metadata)) + suite.addTest(OSComponentTestCase.parameterize( + HeatUtilsCreateComplexStackTests, os_creds=os_creds, ext_net_name=ext_net_name, log_level=log_level, image_metadata=image_metadata)) @@ -415,6 +421,11 @@ def add_openstack_integration_tests(suite, os_creds, ext_net_name, flavor_metadata=flavor_metadata, image_metadata=image_metadata, log_level=log_level)) suite.addTest(OSIntegrationTestCase.parameterize( + CreateComplexStackTests, os_creds=os_creds, + ext_net_name=ext_net_name, use_keystone=use_keystone, + flavor_metadata=flavor_metadata, image_metadata=image_metadata, + log_level=log_level)) + suite.addTest(OSIntegrationTestCase.parameterize( AnsibleProvisioningTests, os_creds=os_creds, ext_net_name=ext_net_name, use_keystone=use_keystone, flavor_metadata=flavor_metadata, image_metadata=image_metadata, diff --git a/snaps/tests/file_utils_tests.py b/snaps/tests/file_utils_tests.py index ef8b4ae..befe37a 100644 --- a/snaps/tests/file_utils_tests.py +++ b/snaps/tests/file_utils_tests.py @@ -38,8 +38,6 @@ class FileUtilsTests(unittest.TestCase): self.tmpFile = self.test_dir + '/bar.txt' self.tmp_file_opened = None - if not os.path.exists(self.tmpFile): - self.tmp_file_opened = open(self.tmpFile, 'wb') def tearDown(self): if self.tmp_file_opened: @@ -70,6 +68,9 @@ class FileUtilsTests(unittest.TestCase): directory """ if not os.path.exists(self.tmpFile): + self.tmp_file_opened = open(self.tmpFile, 'wb') + + if not os.path.exists(self.tmpFile): os.makedirs(self.tmpFile) result = file_utils.file_exists(self.tmpFile) @@ -115,3 +116,20 @@ class FileUtilsTests(unittest.TestCase): self.assertEqual('http://foo:5000/v2.0/', os_env_dict['OS_AUTH_URL']) self.assertEqual('admin', os_env_dict['OS_USERNAME']) self.assertEqual('admin', os_env_dict['OS_TENANT_NAME']) + + def test_write_str_to_file(self): + """ + Ensure the file_utils.fileExists() method returns false with a + directory + """ + test_val = 'test string' + + test_file = file_utils.save_string_to_file( + test_val, self.tmpFile) + result1 = file_utils.file_exists(self.tmpFile) + self.assertTrue(result1) + result2 = file_utils.file_exists(test_file.name) + self.assertTrue(result2) + + file_contents = file_utils.read_file(self.tmpFile) + self.assertEqual(test_val, file_contents) |