Add an extract-ports flag

When the extract-ports flag is used, flame extracts the neutron ports
and connects them (according to the other flags) to :
 - floating ips
 - nova:compute instances
 - security groups
 - subnets and networks

This allows the exported stack to have the same network configuration as
its source.

When the flag is used in conjunction with --generate-stack-data,
the generated stack data also contains the assocation between
ports and floating IPs.

Change-Id: Id2e406c7aac5457cdef822b78edf7e30bd833a2d
This commit is contained in:
Guillaume Espanel 2015-09-17 12:13:31 +02:00
parent ab1f11f7bc
commit 1c858ea907
9 changed files with 878 additions and 78 deletions

View File

@ -37,6 +37,7 @@ Usage
[--os-auth-token OS_AUTH_TOKEN] [--insecure]
[--endpoint_type ENDPOINT_TYPE] [--exclude-servers]
[--exclude-volumes] [--exclude-keypairs] [--generate-stack-data]
[--extract-ports]
Heat template and data file generator
@ -62,6 +63,7 @@ Usage
--generate-stack-data
In addition to template, generate Heat stack data
file.
--extract-ports Export the tenant network ports
Usage example
-------------

View File

@ -14,6 +14,7 @@ Contents:
readme
installation
usage
limitations
contributing
Indices and tables

View File

@ -0,0 +1,18 @@
===========
Limitations
===========
Subnets created before Kilo can have their DHCP server's IP in the middle of
an allocation pool. This causes the first VMs to be allocated the first
free IP addresses of a pool, for example, with a pool starting at 10.0.0.2 :
- 10.0.0.2 : vm1
- 10.0.0.3 : vm2
- 10.0.0.4 : DHCP server
- 10.0.0.5 : vm3
When this stack is imported in Heat, the DHCP server IP is set to the lowest
free IP address of its pool. Depending on the VM creation order, the DHCP
address can either collide with vm1's or vm2's IP.

View File

@ -13,6 +13,7 @@ To use the CLI of flame::
[--os-auth-token OS_AUTH_TOKEN] [--insecure]
[--endpoint_type ENDPOINT_TYPE] [--exclude-servers]
[--exclude-volumes] [--exclude-keypairs] [--generate-stack-data]
[--extract-ports]
Heat template and data file generator
@ -38,6 +39,7 @@ To use the CLI of flame::
--generate-stack-data
In addition to template, generate Heat stack data
file.
--extract-ports Export the tenant network ports
Example

View File

@ -34,11 +34,12 @@ class Client(object):
**kwargs)
def generate(self, exclude_servers, exclude_volumes, exclude_keypairs,
generate_stack_data):
generate_stack_data, extract_ports=False):
self.template_generator.extract_vm_details(exclude_servers,
exclude_volumes,
exclude_keypairs,
generate_stack_data
generate_stack_data,
extract_ports
)
self.template_generator.extract_data()
return self.template_generator.heat_template_and_data()

View File

@ -80,6 +80,9 @@ def main(args=None):
default=False,
help="In addition to template, generate Heat "
"stack data file.")
parser.add_argument('--extract-ports', action='store_true',
default=False,
help="Export the tenant network ports")
args = parser.parse_args()
flame = client.Client(args.username, args.password,
@ -92,7 +95,8 @@ def main(args=None):
template.extract_vm_details(args.exclude_servers,
args.exclude_volumes,
args.exclude_keypairs,
args.generate_stack_data)
args.generate_stack_data,
args.extract_ports)
template.extract_data()
print("### Heat Template ###")
print(template.heat_template_and_data())

View File

@ -158,12 +158,25 @@ class TemplateGenerator(object):
res_type = future_res[res_available]
yield res_type, fetch_map[res_type][1](res)
def order_ports(self):
for i, port in self.ports.values():
for fixed_ip in port['fixed_ips']:
ip_subnet = self.subnets[fixed_ip['subnet_id']][1]
pools = ip_subnet.get('allocation_pools')
if pools:
pools_starts = [pool['start'] for pool in pools]
if fixed_ip['ip_address'] in pools_starts:
# Its the first port of the subnet
ip_subnet['first_port'] = port
def extract_vm_details(self, exclude_servers, exclude_volumes,
exclude_keypairs, generate_data):
exclude_keypairs, generate_data,
extract_ports=False):
self.exclude_servers = exclude_servers
self.exclude_volumes = exclude_volumes
self.exclude_keypairs = exclude_keypairs
self.generate_data = generate_data
self.extract_ports = extract_ports
self.external_networks = []
fetch_map = {
'subnets': (self.neutron.subnet_list, self.build_data),
@ -174,6 +187,7 @@ class TemplateGenerator(object):
'floatingips': (self.neutron.floatingip_list, lambda x: x),
'ports': (self.neutron.port_list, self.build_data),
}
if not exclude_keypairs:
fetch_map['keys'] = (self.nova.keypair_list,
lambda l: {key.name: (index, key) for
@ -189,6 +203,7 @@ class TemplateGenerator(object):
for res_type, result in self.async_fetch_data(fetch_map):
self.__setattr__(res_type, result)
self.order_ports()
def build_data(self, data):
if not data:
@ -289,6 +304,22 @@ class TemplateGenerator(object):
def get_subnet_resource_name(self, subnet_id):
return "subnet_%d" % self.subnets[subnet_id][0]
def get_server_resource_name(self, device_id):
return "server_%d" % self.servers[device_id][0]
def get_router_resource_name(self, device_id):
return "router_%d" % self.routers[device_id][0]
def get_secgroup_resource_name(self, secgroup_id):
return "security_group_%d" % self.secgroups[secgroup_id][0]
def get_ports_for_server(self, server_id):
ports = []
for n, port in self.ports.values():
if port['device_id'] == server_id:
ports.append("port_%d" % n)
return ports
def _extract_subnets(self):
resources = []
for n, subnet in self.subnets.values():
@ -311,11 +342,56 @@ class TemplateGenerator(object):
resources.append(resource)
return resources
def _extract_ports(self):
resources = []
resources_dict = {}
self.dhcp_fixed_ips = {}
for n, port in self.ports.values():
fixed_ips = []
for fixed_ip_dict in port['fixed_ips']:
subnet_id = fixed_ip_dict['subnet_id']
subnet_name = self.get_subnet_resource_name(subnet_id)
fixed_ip_resource = {u'subnet_id':
{'get_resource': subnet_name},
u'ip_address': fixed_ip_dict['ip_address']
}
fixed_ips.append(fixed_ip_resource)
if port['device_owner'] == 'network:dhcp':
# Add the fixed ip to the dhcp_fixed_ips list
dhcp_ips = self.dhcp_fixed_ips.setdefault(subnet_name, [])
dhcp_ips.append(fixed_ip_dict['ip_address'])
if not port['device_owner'].startswith('compute:'):
# It's not a server, skip it!
continue
net_name = self.get_network_resource_name(port['network_id'])
properties = {
'network_id': {'get_resource': net_name},
'admin_state_up': port['admin_state_up'],
'fixed_ips': fixed_ips,
'mac_address': port['mac_address'],
'device_owner': port['device_owner'],
}
if port['name'] != '':
# This port has a name
properties['name'] = port['name']
resource = Resource("port_%d" % n, 'OS::Neutron::Port',
port['id'], properties)
security_groups = self.build_port_secgroups(resource, port)
properties['security_groups'] = security_groups
resources.append(resource)
resources_dict[port['id']] = resource
return resources
def _build_rules(self, rules):
brules = []
for rule in rules:
if rule['protocol'] == 'any':
del rule['protocol']
del rule['port_range_min']
del rule['port_range_max']
rg_id = rule['remote_group_id']
if rg_id is not None:
rule['remote_mode'] = "remote_group_id"
@ -396,6 +472,31 @@ class TemplateGenerator(object):
return security_groups
def build_port_secgroups(self, resource, port):
security_groups = []
port_secgroups = [self.secgroups[sgid][1]
for sgid in port['security_groups']]
secgroup_default_parameter = None
for secgr in port_secgroups:
if secgr['name'] == 'default' and self.generate_data:
if not secgroup_default_parameter:
port_res_name = 'port_%d' % self.ports[port['id']][0]
param_name = "%s_default_security_group" % port_res_name
description = ("Default security group for port %s" %
port['name'])
default = secgr['id']
resource.add_parameter(param_name, description,
default=default)
secgroup_default_parameter = {'get_param': param_name}
security_groups.append(secgroup_default_parameter)
else:
resource_name = ("security_group_%d" %
self.secgroups[secgr['id']][0])
security_groups.append({'get_resource': resource_name})
return security_groups
def build_networks(self, addresses):
networks = []
for net_name in addresses:
@ -460,13 +561,19 @@ class TemplateGenerator(object):
resource_key = "key_%d" % self.keys[server.key_name][0]
properties['key_name'] = {'get_resource': resource_key}
security_groups = self.build_secgroups(resource, server)
if security_groups:
properties['security_groups'] = security_groups
if self.extract_ports:
ports = [{"port": {"get_resource": port}}
for port in self.get_ports_for_server(server.id)]
if ports:
properties['networks'] = ports
else:
security_groups = self.build_secgroups(resource, server)
if security_groups:
properties['security_groups'] = security_groups
networks = self.build_networks(server.addresses)
if networks:
properties['networks'] = networks
networks = self.build_networks(server.addresses)
if networks:
properties['networks'] = networks
if server.metadata:
properties['metadata'] = server.metadata
@ -526,20 +633,36 @@ class TemplateGenerator(object):
default=default)
resources.append(resource)
if not self.exclude_servers and ip['port_id']:
device = self.ports[ip['port_id']][1]['device_id']
if device and self.servers[device]:
server = self.servers[device]
server_resource_name = "server_%d" % server[0]
properties = {
'floating_ip': {'get_resource': ip_resource_name},
'server_id': {'get_resource': server_resource_name}
}
resource = Resource("floatingip_association_%d" % n,
'OS::Nova::FloatingIPAssociation',
None,
properties)
resources.append(resource)
if self.extract_ports and ip['port_id']:
port_number = self.ports[ip['port_id']][0]
port_resource_name = "port_%d" % port_number
properties = {
'floatingip_id': {'get_resource': ip_resource_name},
'port_id': {'get_resource': port_resource_name}
}
resource_id = ("%s:%s" %
(ip['id'],
ip['port_id']))
resource = Resource("floatingip_association_%d" % n,
'OS::Neutron::FloatingIPAssociation',
resource_id,
properties)
resources.append(resource)
else:
if not self.exclude_servers and ip['port_id']:
device = self.ports[ip['port_id']][1]['device_id']
if device and self.servers[device]:
server = self.servers[device]
server_resource_name = "server_%d" % server[0]
properties = {
'floating_ip': {'get_resource': ip_resource_name},
'server_id': {'get_resource': server_resource_name}
}
resource = Resource("floatingip_association_%d" % n,
'OS::Nova::FloatingIPAssociation',
None,
properties)
resources.append(resource)
return resources
def _extract_volumes(self):
@ -596,6 +719,9 @@ class TemplateGenerator(object):
def extract_data(self):
resources = self._extract_routers()
resources += self._extract_networks()
if self.extract_ports:
resources += self._extract_ports()
resources += self._extract_subnets()
resources += self._extract_secgroups()
resources += self._extract_floating()

View File

@ -22,6 +22,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import mock
import yaml
@ -274,11 +275,12 @@ class BaseTestCase(base.TestCase):
super(BaseTestCase, self).tearDown()
def get_generator(self, exclude_servers, exclude_volumes,
exclude_keypairs, generate_data):
exclude_keypairs, generate_data, extract_ports):
generator = flame.TemplateGenerator('x', 'x', 'x', 'x', True,
'publicURL')
generator.extract_vm_details(exclude_servers, exclude_volumes,
exclude_keypairs, generate_data)
exclude_keypairs, generate_data,
extract_ports)
return generator
def check_stackdata(self, resources, expected_resources):
@ -304,7 +306,7 @@ class BaseTestCase(base.TestCase):
class TemplateGenerationTest(BaseTestCase):
def test_heat_template_and_data_with_data(self):
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
generator.extract_data()
out = yaml.load(generator.heat_template_and_data())
mandatory_keys = set(('environment', 'template', 'resources',
@ -316,7 +318,7 @@ class TemplateGenerationTest(BaseTestCase):
self.assertEqual(generator.stack_data, out)
def test_heat_template_and_data_without_data(self):
generator = self.get_generator(False, False, False, False)
generator = self.get_generator(False, False, False, False, False)
generator.extract_data()
out = yaml.load(generator.heat_template_and_data())
mandatory_keys = {'heat_template_version', 'resources', 'description',
@ -343,7 +345,7 @@ class ClientTest(BaseTestCase):
self.assertNotIn('template', parsed_out.keys())
def test_generate_contains_extract(self):
out = self.c.generate(False, False, False, True)
out = self.c.generate(False, False, False, True, False)
parsed_out = yaml.load(out)
self.assertIsInstance(parsed_out, dict)
self.assertIn('template', parsed_out.keys())
@ -353,7 +355,7 @@ class StackDataTests(BaseTestCase):
def test_keypair(self):
self.mock_nova.return_value = FakeNovaManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'key_0': {
@ -370,7 +372,7 @@ class StackDataTests(BaseTestCase):
def test_router(self):
self.mock_neutron.return_value = FakeNeutronManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'router_0': {
@ -393,8 +395,10 @@ class StackDataTests(BaseTestCase):
'external_gateway_info': {
'network_id': '8765',
'enable_snat': 'true'}}, ]
# This router has no port
fake.ports = []
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'router_0': {
@ -447,7 +451,7 @@ class StackDataTests(BaseTestCase):
'id': '1111'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'router_0': {
@ -473,7 +477,7 @@ class StackDataTests(BaseTestCase):
def test_network(self):
self.mock_neutron.return_value = FakeNeutronManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'network_0': {
@ -493,7 +497,7 @@ class StackDataTests(BaseTestCase):
fake.networks[0]['router:external'] = True
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
self.check_stackdata(generator._extract_networks(), {})
@ -512,7 +516,7 @@ class StackDataTests(BaseTestCase):
'id': '1111'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'subnet_0': {
@ -538,7 +542,7 @@ class StackDataTests(BaseTestCase):
'id': '2222'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(True, False, False, True)
generator = self.get_generator(True, False, False, True, False)
expected = {
'floatingip_0': {
@ -572,7 +576,7 @@ class StackDataTests(BaseTestCase):
'id': '1234'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'security_group_0': {
@ -606,14 +610,14 @@ class StackDataTests(BaseTestCase):
'id': '1234'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
self.check_stackdata(generator._extract_secgroups(), {})
def test_volume(self):
self.mock_cinder.return_value = FakeCinderManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'volume_0': {
@ -631,7 +635,7 @@ class StackDataTests(BaseTestCase):
def test_server(self):
self.mock_nova.return_value = FakeNovaManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'server_0': {
@ -658,7 +662,7 @@ class StackDataTests(BaseTestCase):
self.mock_neutron.return_value = fake_neutron
self.mock_nova.return_value = fake_nova
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'server_0': {
@ -675,7 +679,7 @@ class StackDataTests(BaseTestCase):
def test_servergroup(self):
self.mock_nova.return_value = FakeNovaManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'servergroup_0': {
@ -695,7 +699,7 @@ class NetworkTests(BaseTestCase):
def test_keypair(self):
self.mock_nova.return_value = FakeNovaManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'key_0': {
@ -710,7 +714,8 @@ class NetworkTests(BaseTestCase):
def test_router(self):
self.mock_neutron.return_value = FakeNeutronManager()
generator = self.get_generator(False, False, False, True)
self.mock_neutron.return_value.ports = []
generator = self.get_generator(False, False, False, True, False)
expected = {
'router_0': {
@ -725,6 +730,7 @@ class NetworkTests(BaseTestCase):
def test_router_with_external_gateway(self):
fake = FakeNeutronManager()
fake.ports = []
fake.routers = [{'name': 'myrouter',
'id': '1234',
'admin_state_up': 'true',
@ -732,7 +738,7 @@ class NetworkTests(BaseTestCase):
'network_id': '8765',
'enable_snat': 'true'}}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'router_0_external_network': {
@ -791,7 +797,7 @@ class NetworkTests(BaseTestCase):
'id': '1111'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'router_0': {
@ -813,7 +819,7 @@ class NetworkTests(BaseTestCase):
def test_network(self):
self.mock_neutron.return_value = FakeNeutronManager()
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'network_0': {
@ -832,7 +838,7 @@ class NetworkTests(BaseTestCase):
fake.networks[0]['router:external'] = True
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
self.check_template(generator._extract_networks(), {})
@ -851,7 +857,7 @@ class NetworkTests(BaseTestCase):
'id': '1111'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'subnet_0': {
@ -882,7 +888,7 @@ class NetworkTests(BaseTestCase):
'id': '2222'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(True, False, False, False)
generator = self.get_generator(True, False, False, False, False)
expected_parameters = {
'external_network_for_floating_ip_0': {
@ -963,7 +969,7 @@ class NetworkTests(BaseTestCase):
'id': '1234'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, False)
generator = self.get_generator(False, False, False, False, False)
expected = {
'security_group_0': {
@ -1073,7 +1079,7 @@ class NetworkTests(BaseTestCase):
'id': '1111'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, False)
generator = self.get_generator(False, False, False, False, False)
expected = {
'security_group_0': {
@ -1229,7 +1235,7 @@ class NetworkTests(BaseTestCase):
'id': '2222'}, ]
self.mock_neutron.return_value = fake
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected = {
'security_group_0': {
@ -1317,7 +1323,7 @@ class VolumeTests(BaseTestCase):
self.mock_cinder.return_value = self.fake
def test_basic(self):
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_volume_type': {
@ -1342,7 +1348,7 @@ class VolumeTests(BaseTestCase):
def test_basic_unnamed(self):
self.fake.volumes = [FakeUnnamedVolume(), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_volume_type': {
@ -1365,7 +1371,7 @@ class VolumeTests(BaseTestCase):
def test_source_volid_external(self):
self.fake.volumes = [FakeVolume(source_volid=5678), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_source_volid': {
@ -1395,7 +1401,7 @@ class VolumeTests(BaseTestCase):
def test_source_volid_included(self):
self.fake.volumes = [FakeVolume(source_volid=5678),
FakeVolume(id=5678)]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_volume_type': {
@ -1448,7 +1454,7 @@ class VolumeTests(BaseTestCase):
'size': '25'}
self.fake.volumes = [FakeVolume(bootable='true',
volume_image_metadata=metadata), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_volume_type': {
@ -1479,7 +1485,7 @@ class VolumeTests(BaseTestCase):
def test_snapshot_id(self):
self.fake.volumes = [FakeVolume(snapshot_id=5678), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_snapshot_id': {
@ -1510,7 +1516,7 @@ class VolumeTests(BaseTestCase):
def test_volume_type(self):
self.fake.volumes = [FakeVolume(volume_type='isci'), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_volume_type': {
@ -1535,7 +1541,7 @@ class VolumeTests(BaseTestCase):
def test_metadata(self):
self.fake.volumes = [FakeVolume(metadata={'key': 'value'}), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'volume_0_volume_type': {
@ -1568,7 +1574,7 @@ class ServerTests(BaseTestCase):
self.mock_nova.return_value = self.fake
def test_basic(self):
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1599,7 +1605,7 @@ class ServerTests(BaseTestCase):
def test_keypair(self):
self.fake.servers = [FakeServer(key_name='testkey')]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1645,7 +1651,7 @@ class ServerTests(BaseTestCase):
bootable='true')]
self.mock_cinder.return_value = fake_cinder
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1683,7 +1689,7 @@ class ServerTests(BaseTestCase):
server_args = {"id": 777,
"os-extended-volumes:volumes_attached": [{'id': 5678}]}
self.fake.servers = [FakeServer(**server_args), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1723,7 +1729,7 @@ class ServerTests(BaseTestCase):
"description": "Group"}, ]
self.mock_neutron.return_value = fake_neutron
self.fake.groups = {'server1': [FakeSecurityGroup(), ]}
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1760,7 +1766,7 @@ class ServerTests(BaseTestCase):
def test_config_drive(self):
self.fake.servers = [FakeServer(config_drive="True"), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1792,7 +1798,7 @@ class ServerTests(BaseTestCase):
def test_metadata(self):
self.fake.servers = [FakeServer(metadata={"key": "value"}), ]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1839,7 +1845,7 @@ class ServerTests(BaseTestCase):
self.mock_neutron.return_value = fake_neutron
addresses = {"private": [{"addr": "10.0.0.2"}]}
self.fake.servers = [FakeServer(addresses=addresses)]
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1883,7 +1889,7 @@ class ServerTests(BaseTestCase):
server_args = {"id": 777,
"os-extended-volumes:volumes_attached": [{'id': 5678}]}
self.fake.servers = [FakeServer(**server_args), ]
generator = self.get_generator(False, True, False, False)
generator = self.get_generator(False, True, False, False, False)
expected_parameters = {
'server_0_flavor': {
@ -1924,7 +1930,7 @@ class ServerTests(BaseTestCase):
def test_servergroup(self):
self.fake.servers = [FakeServer()]
self.fake.servers[0].id = '12345'
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -1966,7 +1972,7 @@ class GenerationTests(BaseTestCase):
def test_generation(self):
generator = self.get_generator(False, False, False, True)
generator = self.get_generator(False, False, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -2098,7 +2104,7 @@ class GenerationTests(BaseTestCase):
def test_generation_exclude_servers(self):
generator = self.get_generator(True, False, False, True)
generator = self.get_generator(True, False, False, True, False)
expected_parameters = {
'volume_0_volume_type': {
@ -2204,7 +2210,7 @@ class GenerationTests(BaseTestCase):
def test_generation_exclude_volumes(self):
generator = self.get_generator(False, True, False, True)
generator = self.get_generator(False, True, False, True, False)
expected_parameters = {
'server_0_flavor': {
@ -2314,7 +2320,7 @@ class GenerationTests(BaseTestCase):
def test_generation_exclude_keypairs(self):
generator = self.get_generator(False, False, True, True)
generator = self.get_generator(False, False, True, True, False)
expected_parameters = {
'server_0_flavor': {
@ -2437,7 +2443,7 @@ class GenerationTests(BaseTestCase):
def test_generation_exclude_servers_and_volumes(self):
generator = self.get_generator(True, True, False, True)
generator = self.get_generator(True, True, False, True, False)
expected_parameters = {}
expected_resources = {
@ -2517,7 +2523,7 @@ class GenerationTests(BaseTestCase):
def test_generation_exclude_servers_volumes_keypairs(self):
generator = self.get_generator(True, True, True, True)
generator = self.get_generator(True, True, True, True, False)
expected_parameters = {}
expected_resources = {

View File

@ -0,0 +1,640 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014 Cloudwatt
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import mock
from flameclient import flame
from flameclient.tests import base
class FakeBase(object):
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class FakeVolume(FakeBase):
id = 1234
size = 1
source_volid = None
bootable = 'false'
snapshot_id = None
display_name = 'vol1'
display_description = 'Description'
volume_type = 'fast'
metadata = None
class FakeServer(FakeBase):
id = '1234'
name = 'server1'
config_drive = None
flavor = {'id': '2'}
image = {'id': '3333',
'links': [{'href': 'http://p/7777/images/3333',
'rel': 'bookmark'}]}
key_name = 'testkey'
addresses = []
metadata = None
def __init__(self, server_id, **kwargs):
self.id = server_id
kwargs.setdefault('OS-DCF:diskConfig', 'MANUAL')
kwargs.setdefault('os-extended-volumes:volumes_attached', [])
super(FakeServer, self).__init__(**kwargs)
class FakeFlavor(FakeBase):
name = 'm1.tiny'
id = '1'
class FakeKeypair(FakeBase):
name = 'key'
id = 'key'
public_key = 'ssh-rsa AAAAB3NzaC'
class FakeSecurityGroup(FakeBase):
id = '1'
name = 'name'
class FakeNeutronManager(object):
def __init__(self):
self.groups = [{u'description': u'default',
u'id': u'secgorup1',
u'name': u'default',
u'security_group_rules': [
{u'direction': u'ingress',
u'ethertype': u'IPv4',
u'id': u'secgroup-rule1',
u'port_range_max': 65535,
u'port_range_min': 1,
u'protocol': u'tcp',
u'remote_group_id': None,
u'remote_ip_prefix': u'0.0.0.0/0',
u'security_group_id': u'secgorup1',
u'tenant_id': u'tenant1'},
],
u'tenant_id': u'tenant1'}]
self.routers = [
{u'admin_state_up': True,
u'external_gateway_info': {u'enable_snat': True,
u'network_id': u'network3'},
u'id': u'router1',
u'name': u'gw-internal-a',
u'routes': [],
u'status': u'ACTIVE',
u'tenant_id': u'tenant1'},
]
self.ports = [{u'admin_state_up': True,
u'allowed_address_pairs': [],
u'binding:vnic_type': u'normal',
u'device_id': u'router1',
u'device_owner': u'network:router_interface',
u'extra_dhcp_opts': [],
u'fixed_ips': [{u'ip_address': u'192.168.203.1',
u'subnet_id': u'subnet3'}],
u'id': u'port1',
u'mac_address': u'fa:16:3e:fe:c1:b3',
u'name': u'',
u'network_id': u'network1',
u'security_groups': [],
u'status': u'ACTIVE',
u'tenant_id': u'tenant1'},
{u'admin_state_up': True,
u'allowed_address_pairs': [],
u'binding:vnic_type': u'normal',
u'device_id': u'server3',
u'device_owner': u'compute:nova',
u'extra_dhcp_opts': [],
u'fixed_ips': [{u'ip_address': u'192.168.203.5',
u'subnet_id': u'subnet3'}],
u'id': u'port2',
u'mac_address': u'fa:16:3e:e4:44:7b',
u'name': u'',
u'network_id': u'network1',
u'security_groups': [u'secgorup1'],
u'status': u'ACTIVE',
u'tenant_id': u'tenant1'},
{u'admin_state_up': True,
u'allowed_address_pairs': [],
u'binding:vnic_type': u'normal',
u'device_id': u'server2',
u'device_owner': u'compute:nova',
u'extra_dhcp_opts': [],
u'fixed_ips': [{u'ip_address': u'192.168.203.4',
u'subnet_id': u'subnet3'}],
u'id': u'port3',
u'mac_address': u'fa:16:3e:e8:e4:e2',
u'name': u'',
u'network_id': u'network1',
u'security_groups': [u'secgorup1'],
u'status': u'ACTIVE',
u'tenant_id': u'tenant1'},
{u'admin_state_up': True,
u'allowed_address_pairs': [],
u'binding:vnic_type': u'normal',
u'device_id': u'dhcp1-network1',
u'device_owner': u'network:dhcp',
u'extra_dhcp_opts': [],
u'fixed_ips': [{u'ip_address': u'192.168.203.3',
u'subnet_id': u'subnet3'},
{u'ip_address': u'192.168.204.2',
u'subnet_id': u'subnet4'}],
u'id': u'port4',
u'mac_address': u'fa:16:3e:af:86:30',
u'name': u'',
u'network_id': u'network1',
u'security_groups': [],
u'status': u'ACTIVE',
u'tenant_id': u'tenant1'},
{u'admin_state_up': True,
u'allowed_address_pairs': [],
u'binding:vnic_type': u'normal',
u'device_id': u'server1',
u'device_owner': u'compute:nova',
u'extra_dhcp_opts': [],
u'fixed_ips': [{u'ip_address': u'192.168.203.2',
u'subnet_id': u'subnet3'}],
u'id': u'port6',
u'mac_address': u'fa:16:3e:b0:9a:e2',
u'name': u'',
u'network_id': u'network1',
u'security_groups': [u'secgorup1'],
u'status': u'ACTIVE',
u'tenant_id': u'tenant1'}
]
self.subnets = [{u'allocation_pools': [
{u'end': u'172.19.0.254', u'start': u'172.19.0.2'}],
u'cidr': u'172.19.0.0/24',
u'dns_nameservers': [],
u'enable_dhcp': True,
u'gateway_ip': u'172.19.0.1',
u'host_routes': [],
u'id': u'subnet1',
u'ip_version': 4,
u'name': u'storage',
u'network_id': u'network2',
u'tenant_id': u'tenant1'},
{u'allocation_pools': [
{u'end': u'10.8.8.200',
u'start': u'10.8.8.100'}],
u'cidr': u'10.8.8.0/24',
u'dns_nameservers': [],
u'enable_dhcp': False,
u'gateway_ip': u'10.8.8.254',
u'host_routes': [],
u'id': u'subnet2',
u'ip_version': 4,
u'name': u'ext-subnet',
u'network_id': u'network3',
u'tenant_id': u'tenant1'},
{u'allocation_pools': [{u'end': u'192.168.203.254',
u'start': u'192.168.203.2'}],
u'cidr': u'192.168.203.0/24',
u'dns_nameservers': [],
u'enable_dhcp': True,
u'gateway_ip': u'192.168.203.1',
u'host_routes': [],
u'id': u'subnet3',
u'ip_version': 4,
u'name': u'int-a-1',
u'network_id': u'network1',
u'tenant_id': u'tenant1'},
{u'allocation_pools': [{u'end': u'192.168.204.254',
u'start': u'192.168.204.2'}],
u'cidr': u'192.168.204.0/24',
u'dns_nameservers': [],
u'enable_dhcp': True,
u'gateway_ip': u'192.168.204.1',
u'host_routes': [],
u'id': u'subnet4',
u'ip_version': 4,
u'name': u'int-a-2',
u'network_id': u'network1',
u'tenant_id': u'tenant1'}]
self.networks = [{u'admin_state_up': True,
u'id': u'network1',
u'name': u'internal',
u'router:external': False,
u'shared': False,
u'status': u'ACTIVE',
u'subnets': [u'subnet3',
u'subnet4'],
u'tenant_id': u'tenant1'},
{u'admin_state_up': True,
u'id': u'network2',
u'name': u'storage',
u'router:external': False,
u'shared': False,
u'status': u'ACTIVE',
u'subnets': [u'subnet1'],
u'tenant_id': u'tenant1'},
{u'admin_state_up': True,
u'id': u'network3',
u'name': u'ext-net',
u'router:external': True,
u'shared': True,
u'status': u'ACTIVE',
u'subnets': [u'subnet2'],
u'tenant_id': u'tenant1'}]
self.floatingips = [{u'fixed_ip_address': None,
u'floating_ip_address': u'10.8.8.102',
u'floating_network_id': u'network3',
u'id': u'floating1',
u'port_id': None,
u'router_id': None,
u'status': u'DOWN',
u'tenant_id': u'tenant1'},
{u'fixed_ip_address': None,
u'floating_ip_address': u'10.8.8.101',
u'floating_network_id': u'network3',
u'id': u'floating2',
u'port_id': None,
u'router_id': None,
u'status': u'DOWN',
u'tenant_id': u'tenant1'},
{u'fixed_ip_address': u'192.168.203.4',
u'floating_ip_address': u'10.8.8.168',
u'floating_network_id': u'network3',
u'id': u'floating3',
u'port_id': u'port3',
u'router_id': u'router1',
u'status': u'ACTIVE',
u'tenant_id': u'tenant1'},
{u'fixed_ip_address': None,
u'floating_ip_address': u'10.8.8.118',
u'floating_network_id': u'network3',
u'id': u'floating4',
u'port_id': None,
u'router_id': None,
u'status': u'DOWN',
u'tenant_id': u'tenant1'}]
def subnet_list(self):
return self.subnets
def network_list(self):
return self.networks
def port_list(self):
return self.ports
def router_list(self):
return self.routers
def router_interfaces_list(self, router):
return [port for port in self.ports
if port['device_id'] == router['id']]
def secgroup_list(self):
return self.groups
def floatingip_list(self):
return self.floatingips
class FakeNovaManager(object):
def __init__(self):
self.servers = [FakeServer('server1'),
FakeServer('server2'),
FakeServer('server3')]
self.servergroups = []
self.flavors = [FakeFlavor(id='2', name='m1.small')]
self.groups = {}
self.keypairs = [FakeKeypair(name='testkey',
public_key='ssh-rsa XXXX')]
def keypair_list(self):
return self.keypairs
def flavor_list(self):
return self.flavors
def server_list(self):
return self.servers
def server_security_group_list(self, server):
return self.groups.get(server.name, [])
def servergroup_list(self):
return self.servergroups
class FakeCinderManager(object):
def __init__(self):
self.volumes = [FakeVolume(), ]
def volume_list(self):
return self.volumes
class ResourceTestCase(base.TestCase):
def test_template_resource(self):
resource = flame.Resource('my-name',
'my-type',
properties='my-properties')
expected = {
'my-name': {
'type': 'my-type',
'properties': 'my-properties',
}
}
self.assertEqual(expected, resource.template_resource)
class BaseTestCase(base.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()
self.patch_neutron = mock.patch('flameclient.managers.NeutronManager')
self.mock_neutron = self.patch_neutron.start()
self.patch_nova = mock.patch('flameclient.managers.NovaManager')
self.mock_nova = self.patch_nova.start()
self.patch_cinder = mock.patch('flameclient.managers.CinderManager')
self.mock_cinder = self.patch_cinder.start()
def tearDown(self):
self.mock_neutron.stop()
self.mock_nova.stop()
self.mock_cinder.stop()
super(BaseTestCase, self).tearDown()
def get_generator(self, exclude_servers, exclude_volumes,
exclude_keypairs, generate_data, extract_ports):
generator = flame.TemplateGenerator('x', 'x', 'x', 'x', True,
'publicURL')
generator.extract_vm_details(exclude_servers, exclude_volumes,
exclude_keypairs, generate_data,
extract_ports)
return generator
def check_stackdata(self, resources, expected_resources):
merged_resources = {}
for resource in resources:
merged_resources.update(resource.stack_resource)
self.assertEqual(expected_resources, merged_resources)
def check_template(self, resources, expected_resources,
expected_parameters=None):
expected_parameters = expected_parameters or {}
merged_resources = {}
merged_parameters = {}
for resource in resources:
merged_resources.update(resource.template_resource)
merged_parameters.update(resource.template_parameter)
self.assertEqual(expected_resources, merged_resources)
self.assertEqual(expected_parameters, merged_parameters)
class StackDataTests(BaseTestCase):
def setUp(self):
super(StackDataTests, self).setUp()
self.mock_neutron.return_value = FakeNeutronManager()
self.mock_nova.return_value = FakeNovaManager()
self.mock_cinder.return_value = FakeCinderManager()
def test_routers_presents(self):
generator = self.get_generator(False, False, False, True, True)
extraction = generator._extract_routers()
routers = {r.name: r for r in extraction}
self.assertIn('router_0', routers)
def test_routers_resource_names(self):
generator = self.get_generator(False, False, False, True, True)
generator_output = generator._extract_routers()
routers = (res for res in generator_output
if res.type == "OS::Neutron::Router")
for n, router in enumerate(routers):
assert(router.name.startswith("router_"))
def test_ports_presents(self):
generator = self.get_generator(False, False, False, True, True)
extraction = generator._extract_ports()
ports = {r.name: r for r in extraction}
self.assertIn('port_1', ports)
self.assertIn('port_2', ports)
def test_ports_resource_names_types(self):
generator = self.get_generator(False, False, False, True, True)
extraction = generator._extract_ports()
for n, port in enumerate(extraction):
props = port.properties
assert(extraction[0].name.startswith("port_"))
self.assertEqual("OS::Neutron::Port", port.type)
self.assertIsInstance(props['admin_state_up'], bool)
self.assertIsInstance(props['security_groups'], list)
assert(props['device_owner'].startswith("compute:"))
def test_port_fixed_ip(self):
reference = [{'ip_address': '192.168.203.2',
'subnet_id': {'get_resource': 'subnet_2'}}]
generator = self.get_generator(False, False, False, True, True)
extraction = generator._extract_ports()
# Get the right port for the test
port = next((p for p in extraction if
p.properties['mac_address'] == 'fa:16:3e:b0:9a:e2'))
props = port.properties
self.assertIsInstance(props['fixed_ips'], list)
fixed_ips = props['fixed_ips']
for ref in reference:
self.assertIn(ref, fixed_ips)
def test_servers_ports_assignations(self):
generator = self.get_generator(False, False, False, True, True)
extraction = generator._extract_servers()
used_ports = []
for n, server in enumerate(extraction):
props = server.properties
self.assertIsInstance(props['networks'], list)
for network in props['networks']:
port = network['port']['get_resource']
assert(port.startswith("port_"))
# Port has not been used by another server
self.assertNotIn(port, used_ports)
used_ports.append(port)
def test_floating_association(self):
generator = self.get_generator(False, False, False, True, True)
extraction = generator._extract_floating()
associations = (res for res in extraction
if res.type == "OS::Neutron::FloatingIPAssociation")
for association in associations:
props = association.properties
assert(props['floatingip_id']['get_resource'].
startswith('floatingip_'))
assert(props['port_id']['get_resource'].
startswith('port_'))
class GenerationTests(BaseTestCase):
resource_ref = set(['floatingip_association_2',
'subnet_2', 'subnet_3', 'subnet_0',
'port_2', 'port_1', 'port_4',
'server_2', 'server_1', 'server_0',
'router_0',
'router_0_interface_0',
'router_0_gateway',
'key_0',
'network_0', 'network_1',
'floatingip_0', 'floatingip_1',
'floatingip_2', 'floatingip_3',
'volume_0'])
params_ref = set(['volume_0_volume_type',
'external_network_for_floating_ip_3',
'external_network_for_floating_ip_2',
'external_network_for_floating_ip_1',
'external_network_for_floating_ip_0',
'port_4_default_security_group',
'port_1_default_security_group',
'port_2_default_security_group',
'router_0_external_network',
'server_1_image',
'server_1_flavor',
'server_1_key',
'server_2_image',
'server_2_flavor',
'server_2_key',
'server_0_image',
'server_0_flavor',
'server_0_key'])
data_ref = set(['floatingip_0', 'floatingip_1', 'floatingip_2',
'floatingip_3',
'floatingip_association_2',
'key_0',
'network_0', 'network_1',
'port_1', 'port_2', 'port_4',
'router_0',
'router_0_gateway',
'router_0_interface_0',
'server_0', 'server_1', 'server_2',
'subnet_0', 'subnet_2', 'subnet_3',
'volume_0'])
def filter_set(self, filtered_set, exclude):
excluded_set = set()
for exc in exclude:
excluded_set.update(
set([e for e in filtered_set if re.search(exc, e)])
)
return filtered_set.difference(excluded_set)
def setUp(self):
super(GenerationTests, self).setUp()
self.mock_neutron.return_value = FakeNeutronManager()
self.mock_nova.return_value = FakeNovaManager()
self.mock_cinder.return_value = FakeCinderManager()
def test_generation(self):
exclusion_table = [
{'call_params': (False, False, False, True, True),
'resource_filter': [],
'params_filter': ['^server_\d+_key$'],
'data_filter': []},
# No server
{'call_params': (True, False, False, True, True),
'resource_filter': ['^server'],
'params_filter': ['^server'],
'data_filter': ['^server']},
# No volumes
{'call_params': (False, True, False, True, True),
'resource_filter': ['^volume'],
'params_filter': [r'^volume_\d+_volume_type$',
'^server_\d+_key$'],
'data_filter': ['^volume']},
# No keys
{'call_params': (False, False, True, True, True),
'resource_filter': ['^key_\d+$'],
'params_filter': [],
'data_filter': ['^key', 'server_\d+_key']},
# No ports
{'call_params': (False, False, False, True, False),
'resource_filter': ['^port_\d+$'],
'params_filter': ['^port_\d+_default_security_group$',
'server_\d+_key$'],
'data_filter': ['^port_\d+', '^floatingip_association_\d+$']},
]
for exclusion in exclusion_table:
generator = self.get_generator(*exclusion['call_params'])
resource_ref = self.filter_set(self.resource_ref,
exclusion['resource_filter'])
params_ref = self.filter_set(self.params_ref,
exclusion['params_filter'])
data_ref = self.filter_set(self.data_ref,
exclusion['data_filter'])
generator.extract_data()
# All the resources, params and datas are present
self.assertEqual(resource_ref,
set(generator.template['resources'].keys()),
"Called with : %r" % (exclusion['call_params'],))
self.assertEqual(params_ref,
set(generator.template['parameters'].keys()),
"Called with : %r" % (exclusion['call_params'],))
self.assertEqual(data_ref,
set(generator.stack_data['resources'].keys()),
"Called with : %r" % (exclusion['call_params'],))
def test_floating_association_data(self):
generator = self.get_generator(False, False, False, True, True)
generator.extract_data()
# Look for floating ips
assoc_name = 'floatingip_association_2'
association_data = generator.stack_data['resources'][assoc_name]
reference = {'action': 'CREATE',
'metadata': {},
'name': 'floatingip_association_2',
'resource_data': {},
'resource_id': u'floating3:port3',
'status': 'COMPLETE',
'type': 'OS::Neutron::FloatingIPAssociation'}
self.assertEqual(reference, association_data)
def test_port_data(self):
generator = self.get_generator(False, False, False, True, True)
generator.extract_data()
# Look for floating ips
assoc_name = 'port_2'
association_data = generator.stack_data['resources'][assoc_name]
reference = {'action': 'CREATE',
'metadata': {},
'name': 'port_2',
'resource_data': {},
'resource_id': u'port3',
'status': 'COMPLETE',
'type': 'OS::Neutron::Port'}
self.assertEqual(reference, association_data)