1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 # and others. All rights reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 from neutronclient.common.exceptions import NotFound
19 from snaps.openstack.utils import keystone_utils, neutron_utils
21 __author__ = 'spisarski'
23 logger = logging.getLogger('OpenStackNetwork')
26 class OpenStackNetwork:
28 Class responsible for creating a network in OpenStack
31 def __init__(self, os_creds, network_settings):
33 Constructor - all parameters are required
34 :param os_creds: The credentials to connect with OpenStack
35 :param network_settings: The settings used to create a network
37 self.__os_creds = os_creds
38 self.network_settings = network_settings
41 # Attributes instantiated on create()
43 self.__subnets = list()
45 def create(self, cleanup=False):
47 Responsible for creating not only the network but then a private subnet, router, and an interface to the router.
48 :param cleanup: When true, only perform lookups for OpenStack objects.
49 :return: the created network object or None
51 self.__neutron = neutron_utils.neutron_client(self.__os_creds)
53 logger.info('Creating neutron network %s...' % self.network_settings.name)
54 net_inst = neutron_utils.get_network(self.__neutron, self.network_settings.name,
55 self.network_settings.get_project_id(self.__os_creds))
57 self.__network = net_inst
60 self.__network = neutron_utils.create_network(self.__neutron, self.__os_creds,
61 self.network_settings)
63 logger.info('Network does not exist and will not create as in cleanup mode')
65 logger.debug("Network '%s' created successfully" % self.__network['network']['id'])
67 logger.debug('Creating Subnets....')
68 for subnet_setting in self.network_settings.subnet_settings:
69 sub_inst = neutron_utils.get_subnet_by_name(self.__neutron, subnet_setting.name)
71 self.__subnets.append(sub_inst)
72 logger.debug("Subnet '%s' created successfully" % sub_inst['subnet']['id'])
75 self.__subnets.append(neutron_utils.create_subnet(self.__neutron, subnet_setting,
76 self.__os_creds, self.__network))
82 Removes and deletes all items created in reverse order.
84 for subnet in self.__subnets:
86 logger.info('Deleting subnet with name ' + subnet['subnet']['name'])
87 neutron_utils.delete_subnet(self.__neutron, subnet)
89 logger.warning('Error deleting subnet with message - ' + str(e))
91 self.__subnets = list()
95 neutron_utils.delete_network(self.__neutron, self.__network)
101 def get_network(self):
103 Returns the created OpenStack network object
104 :return: the OpenStack network object
106 return self.__network
108 def get_subnets(self):
110 Returns the OpenStack subnet objects
113 return self.__subnets
116 class NetworkSettings:
118 Class representing a network configuration
121 def __init__(self, config=None, name=None, admin_state_up=True, shared=None, project_name=None,
122 external=False, network_type=None, physical_network=None, subnet_settings=list()):
124 Constructor - all parameters are optional
125 :param config: Should be a dict object containing the configuration settings using the attribute names below
126 as each member's the key and overrides any of the other parameters.
127 :param name: The network name.
128 :param admin_state_up: The administrative status of the network. True = up / False = down (default True)
129 :param shared: Boolean value indicating whether this network is shared across all projects/tenants. By default,
130 only administrative users can change this value.
131 :param project_name: Admin-only. The name of the project that will own the network. This project can be
132 different from the project that makes the create network request. However, only
133 administrative users can specify a project ID other than their own. You cannot change this
134 value through authorization policies.
135 :param external: when true, will setup an external network (default False).
136 :param network_type: the type of network (i.e. vlan|flat).
137 :param physical_network: the name of the physical network (this is required when network_type is 'flat')
138 :param subnet_settings: List of SubnetSettings objects.
142 self.project_id = None
145 self.name = config.get('name')
146 if config.get('admin_state_up') is not None:
147 self.admin_state_up = bool(config['admin_state_up'])
149 self.admin_state_up = admin_state_up
151 if config.get('shared') is not None:
152 self.shared = bool(config['shared'])
156 self.project_name = config.get('project_name')
158 if config.get('external') is not None:
159 self.external = bool(config.get('external'))
161 self.external = external
163 self.network_type = config.get('network_type')
164 self.physical_network = config.get('physical_network')
166 self.subnet_settings = list()
167 if config.get('subnets'):
168 for subnet_config in config['subnets']:
169 self.subnet_settings.append(SubnetSettings(config=subnet_config['subnet']))
173 self.admin_state_up = admin_state_up
175 self.project_name = project_name
176 self.external = external
177 self.network_type = network_type
178 self.physical_network = physical_network
179 self.subnet_settings = subnet_settings
181 if not self.name or len(self.name) < 1:
182 raise Exception('Name required for networks')
184 def get_project_id(self, os_creds):
186 Returns the project ID for a given project_name or None
187 :param os_creds: the credentials required for keystone client retrieval
188 :return: the ID or None
191 return self.project_id
193 if self.project_name:
194 keystone = keystone_utils.keystone_client(os_creds)
195 project = keystone_utils.get_project(keystone, self.project_name)
201 def dict_for_neutron(self, os_creds):
203 Returns a dictionary object representing this object.
204 This is meant to be converted into JSON designed for use by the Neutron API
206 TODO - expand automated testing to exercise all parameters
208 :param os_creds: the OpenStack credentials
209 :return: the dictionary object
214 out['name'] = self.name
215 if self.admin_state_up is not None:
216 out['admin_state_up'] = self.admin_state_up
218 out['shared'] = self.shared
219 if self.project_name:
220 project_id = self.get_project_id(os_creds)
222 out['project_id'] = project_id
224 raise Exception('Could not find project ID for project named - ' + self.project_name)
225 if self.network_type:
226 out['provider:network_type'] = self.network_type
227 if self.physical_network:
228 out['provider:physical_network'] = self.physical_network
230 out['router:external'] = self.external
231 return {'network': out}
234 class SubnetSettings:
236 Class representing a subnet configuration
239 def __init__(self, config=None, cidr=None, ip_version=4, name=None, project_name=None, start=None,
240 end=None, gateway_ip=None, enable_dhcp=None, dns_nameservers=None, host_routes=None, destination=None,
241 nexthop=None, ipv6_ra_mode=None, ipv6_address_mode=None):
243 Constructor - all parameters are optional except cidr (subnet mask)
244 :param config: Should be a dict object containing the configuration settings using the attribute names below
245 as each member's the key and overrides any of the other parameters.
246 :param cidr: The CIDR. REQUIRED if config parameter is None
247 :param ip_version: The IP version, which is 4 or 6.
248 :param name: The subnet name.
249 :param project_name: The name of the project who owns the network. Only administrative users can specify a
250 project ID other than their own. You cannot change this value through authorization
252 :param start: The start address for the allocation pools.
253 :param end: The end address for the allocation pools.
254 :param gateway_ip: The gateway IP address.
255 :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is disabled.
256 :param dns_nameservers: A list of DNS name servers for the subnet. Specify each name server as an IP address
257 and separate multiple entries with a space. For example [8.8.8.7 8.8.8.8].
258 :param host_routes: A list of host route dictionaries for the subnet. For example:
261 "destination":"0.0.0.0/0",
262 "nexthop":"123.456.78.9"
265 "destination":"192.168.0.0/24",
266 "nexthop":"192.168.0.1"
269 :param destination: The destination for static route
270 :param nexthop: The next hop for the destination.
271 :param ipv6_ra_mode: A valid value is dhcpv6-stateful, dhcpv6-stateless, or slaac.
272 :param ipv6_address_mode: A valid value is dhcpv6-stateful, dhcpv6-stateless, or slaac.
273 :raise: Exception when config does not have or cidr values are None
275 if not dns_nameservers:
276 dns_nameservers = ['8.8.8.8']
279 self.cidr = config['cidr']
280 if config.get('ip_version'):
281 self.ip_version = config['ip_version']
283 self.ip_version = ip_version
285 # Optional attributes that can be set after instantiation
286 self.name = config.get('name')
287 self.project_name = config.get('project_name')
288 self.start = config.get('start')
289 self.end = config.get('end')
290 self.gateway_ip = config.get('gateway_ip')
291 self.enable_dhcp = config.get('enable_dhcp')
293 if config.get('dns_nameservers'):
294 self.dns_nameservers = config.get('dns_nameservers')
296 self.dns_nameservers = dns_nameservers
298 self.host_routes = config.get('host_routes')
299 self.destination = config.get('destination')
300 self.nexthop = config.get('nexthop')
301 self.ipv6_ra_mode = config.get('ipv6_ra_mode')
302 self.ipv6_address_mode = config.get('ipv6_address_mode')
304 # Required attributes
306 self.ip_version = ip_version
308 # Optional attributes that can be set after instantiation
310 self.project_name = project_name
313 self.gateway_ip = gateway_ip
314 self.enable_dhcp = enable_dhcp
315 self.dns_nameservers = dns_nameservers
316 self.host_routes = host_routes
317 self.destination = destination
318 self.nexthop = nexthop
319 self.ipv6_ra_mode = ipv6_ra_mode
320 self.ipv6_address_mode = ipv6_address_mode
322 if not self.name or not self.cidr:
323 raise Exception('Name and cidr required for subnets')
325 def dict_for_neutron(self, os_creds, network=None):
327 Returns a dictionary object representing this object.
328 This is meant to be converted into JSON designed for use by the Neutron API
329 :param os_creds: the OpenStack credentials
330 :param network: (Optional) the network object on which the subnet will be created
331 :return: the dictionary object
335 'ip_version': self.ip_version,
339 out['network_id'] = network['network']['id']
341 out['name'] = self.name
342 if self.project_name:
343 keystone = keystone_utils.keystone_client(os_creds)
344 project = keystone_utils.get_project(keystone, self.project_name)
347 project_id = project.id
349 out['project_id'] = project_id
351 raise Exception('Could not find project ID for project named - ' + self.project_name)
352 if self.start and self.end:
353 out['allocation_pools'] = [{'start': self.start, 'end': self.end}]
355 out['gateway_ip'] = self.gateway_ip
356 if self.enable_dhcp is not None:
357 out['enable_dhcp'] = self.enable_dhcp
358 if self.dns_nameservers and len(self.dns_nameservers) > 0:
359 out['dns_nameservers'] = self.dns_nameservers
360 if self.host_routes and len(self.host_routes) > 0:
361 out['host_routes'] = self.host_routes
363 out['destination'] = self.destination
365 out['nexthop'] = self.nexthop
366 if self.ipv6_ra_mode:
367 out['ipv6_ra_mode'] = self.ipv6_ra_mode
368 if self.ipv6_address_mode:
369 out['ipv6_address_mode'] = self.ipv6_address_mode
375 Class representing a port configuration
378 def __init__(self, config=None, name=None, network_name=None, admin_state_up=True, project_name=None,
379 mac_address=None, ip_addrs=None, fixed_ips=None, security_groups=None, allowed_address_pairs=None,
380 opt_value=None, opt_name=None, device_owner=None, device_id=None):
382 Constructor - all parameters are optional
383 :param config: Should be a dict object containing the configuration settings using the attribute names below
384 as each member's the key and overrides any of the other parameters.
385 :param name: A symbolic name for the port.
386 :param network_name: The name of the network on which to create the port.
387 :param admin_state_up: A boolean value denoting the administrative status of the port. True = up / False = down
388 :param project_name: The name of the project who owns the network. Only administrative users can specify a
389 project ID other than their own. You cannot change this value through authorization
391 :param mac_address: The MAC address. If you specify an address that is not valid, a Bad Request (400) status
392 code is returned. If you do not specify a MAC address, OpenStack Networking tries to
393 allocate one. If a failure occurs, a Service Unavailable (503) status code is returned.
394 :param ip_addrs: A list of dict objects where each contains two keys 'subnet_name' and 'ip' values which will
395 get mapped to self.fixed_ips.
396 These values will be directly translated into the fixed_ips dict
397 :param fixed_ips: A dict where the key is the subnet IDs and value is the IP address to assign to the port
398 :param security_groups: One or more security group IDs.
399 :param allowed_address_pairs: A dictionary containing a set of zero or more allowed address pairs. An address
400 pair contains an IP address and MAC address.
401 :param opt_value: The extra DHCP option value.
402 :param opt_name: The extra DHCP option name.
403 :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent.
404 :param device_id: The ID of the device that uses this port. For example, a virtual server.
410 self.name = config.get('name')
411 self.network_name = config.get('network_name')
413 if config.get('admin_state_up') is not None:
414 self.admin_state_up = bool(config['admin_state_up'])
416 self.admin_state_up = admin_state_up
418 self.project_name = config.get('project_name')
419 self.mac_address = config.get('mac_address')
420 self.ip_addrs = config.get('ip_addrs')
421 self.fixed_ips = config.get('fixed_ips')
422 self.security_groups = config.get('security_groups')
423 self.allowed_address_pairs = config.get('allowed_address_pairs')
424 self.opt_value = config.get('opt_value')
425 self.opt_name = config.get('opt_name')
426 self.device_owner = config.get('device_owner')
427 self.device_id = config.get('device_id')
430 self.network_name = network_name
431 self.admin_state_up = admin_state_up
432 self.project_name = project_name
433 self.mac_address = mac_address
434 self.ip_addrs = ip_addrs
435 self.fixed_ips = fixed_ips
436 self.security_groups = security_groups
437 self.allowed_address_pairs = allowed_address_pairs
438 self.opt_value = opt_value
439 self.opt_name = opt_name
440 self.device_owner = device_owner
441 self.device_id = device_id
443 if not self.name or not self.network_name:
444 raise Exception('The attributes neutron, name, and network_name are required for PortSettings')
446 def __set_fixed_ips(self, neutron):
448 Sets the self.fixed_ips value
449 :param neutron: the Neutron client
452 if not self.fixed_ips and self.ip_addrs:
453 self.fixed_ips = list()
455 for ip_addr_dict in self.ip_addrs:
456 subnet = neutron_utils.get_subnet_by_name(neutron, ip_addr_dict['subnet_name'])
458 self.fixed_ips.append({'ip_address': ip_addr_dict['ip'], 'subnet_id': subnet['subnet']['id']})
460 raise Exception('Invalid port configuration, subnet does not exist with name - ' +
461 ip_addr_dict['subnet_name'])
463 def dict_for_neutron(self, neutron, os_creds):
465 Returns a dictionary object representing this object.
466 This is meant to be converted into JSON designed for use by the Neutron API
468 TODO - expand automated testing to exercise all parameters
469 :param neutron: the Neutron client
470 :param os_creds: the OpenStack credentials
471 :return: the dictionary object
473 self.__set_fixed_ips(neutron)
478 if self.project_name:
479 keystone = keystone_utils.keystone_client(os_creds)
480 project = keystone_utils.get_project(keystone, self.project_name)
482 project_id = project.id
485 self.network = neutron_utils.get_network(neutron, self.network_name, project_id)
487 raise Exception('Cannot locate network with name - ' + self.network_name)
489 out['network_id'] = self.network['network']['id']
491 if self.admin_state_up is not None:
492 out['admin_state_up'] = self.admin_state_up
494 out['name'] = self.name
495 if self.project_name:
497 out['project_id'] = project_id
499 raise Exception('Could not find project ID for project named - ' + self.project_name)
501 out['mac_address'] = self.mac_address
502 if self.fixed_ips and len(self.fixed_ips) > 0:
503 out['fixed_ips'] = self.fixed_ips
504 if self.security_groups:
505 out['security_groups'] = self.security_groups
506 if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
507 out['allowed_address_pairs'] = self.allowed_address_pairs
509 out['opt_value'] = self.opt_value
511 out['opt_name'] = self.opt_name
512 if self.device_owner:
513 out['device_owner'] = self.device_owner
515 out['device_id'] = self.device_id