Coverage for manila/share/drivers/service_instance.py: 96%
587 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2014 NetApp, Inc.
2# Copyright (c) 2015 Mirantis, Inc.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
17"""Module for managing nova instances for share drivers."""
19import abc
20import os
21import time
23import netaddr
24from oslo_config import cfg
25from oslo_log import log
26from oslo_utils import importutils
27from oslo_utils import netutils
29from manila.common import constants as const
30from manila import compute
31from manila import context
32from manila import coordination
33from manila import exception
34from manila.i18n import _
35from manila import image
36from manila.network.linux import ip_lib
37from manila.network.neutron import api as neutron
38from manila import ssh_utils
39from manila import utils
40from manila import volume
42LOG = log.getLogger(__name__)
43NEUTRON_NAME = "neutron"
45share_servers_handling_mode_opts = [
46 cfg.StrOpt(
47 "service_image_name",
48 default="manila-service-image",
49 help="Name of image in Glance, that will be used for service instance "
50 "creation. Only used if driver_handles_share_servers=True."),
51 cfg.StrOpt(
52 "service_instance_name_template",
53 default="%s",
54 help="Name of service instance. "
55 "Only used if driver_handles_share_servers=True."),
56 cfg.StrOpt(
57 "manila_service_keypair_name",
58 default="manila-service",
59 help="Keypair name that will be created and used for service "
60 "instances. Only used if driver_handles_share_servers=True."),
61 cfg.StrOpt(
62 "path_to_public_key",
63 default="~/.ssh/id_rsa.pub",
64 help="Path to hosts public key. "
65 "Only used if driver_handles_share_servers=True."),
66 cfg.StrOpt(
67 "service_instance_security_group",
68 default="manila-service",
69 help="Security group name, that will be used for "
70 "service instance creation. "
71 "Only used if driver_handles_share_servers=True."),
72 cfg.StrOpt(
73 "service_instance_flavor_id",
74 default="100",
75 help="ID of flavor, that will be used for service instance "
76 "creation. Only used if driver_handles_share_servers=True."),
77 cfg.StrOpt(
78 "service_network_name",
79 default="manila_service_network",
80 help="Name of manila service network. Used only with Neutron. "
81 "Only used if driver_handles_share_servers=True."),
82 cfg.HostAddressOpt(
83 "service_network_host",
84 sample_default="<your_network_hostname>",
85 help="Hostname to be used for service network binding. Used only with "
86 "Neutron and if driver_handles_share_servers=True."),
87 cfg.StrOpt(
88 "service_network_cidr",
89 default="10.254.0.0/16",
90 help="CIDR of manila service network. Used only with Neutron and "
91 "if driver_handles_share_servers=True."),
92 cfg.IntOpt(
93 "service_network_division_mask",
94 default=28,
95 help="This mask is used for dividing service network into "
96 "subnets, IP capacity of subnet with this mask directly "
97 "defines possible amount of created service VMs "
98 "per tenant's subnet. Used only with Neutron "
99 "and if driver_handles_share_servers=True."),
100 cfg.StrOpt(
101 "interface_driver",
102 default="manila.network.linux.interface.OVSInterfaceDriver",
103 help="Module path to the Virtual Interface (VIF) driver class. This "
104 "option is used only by drivers operating in "
105 "`driver_handles_share_servers=True` mode that provision "
106 "OpenStack compute instances as share servers. This option is "
107 "only supported with Neutron networking. "
108 "Drivers provided in tree work with Linux Bridge "
109 "(manila.network.linux.interface.BridgeInterfaceDriver) and OVS "
110 "(manila.network.linux.interface.OVSInterfaceDriver). If the "
111 "manila-share service is running on a host that is connected to "
112 "the administrator network, a no-op driver "
113 "(manila.network.linux.interface.NoopInterfaceDriver) may "
114 "be used."),
115 cfg.BoolOpt(
116 "connect_share_server_to_tenant_network",
117 default=False,
118 help="Attach share server directly to share network. "
119 "Used only with Neutron and "
120 "if driver_handles_share_servers=True."),
121 cfg.StrOpt(
122 "admin_network_id",
123 help="ID of neutron network used to communicate with admin network,"
124 " to create additional admin export locations on."),
125 cfg.StrOpt(
126 "admin_subnet_id",
127 help="ID of neutron subnet used to communicate with admin network,"
128 " to create additional admin export locations on. "
129 "Related to 'admin_network_id'."),
130 cfg.BoolOpt(
131 'service_instance_boot_from_volume',
132 default=False,
133 help='Boot service instances (share servers) from a Cinder volume. '
134 'If False, boot from the image as before. '
135 'Only used if driver_handles_share_servers=True.'
136 ),
137 cfg.IntOpt(
138 'service_instance_boot_volume_size',
139 default=10,
140 min=1,
141 help='Size (GiB) of the root volume when booting from volume. '
142 'Only used if driver_handles_share_servers=True.'
143 ),
144 cfg.StrOpt(
145 'service_instance_boot_volume_type',
146 help='Name or id of cinder volume type which will be used '
147 'for all boot volumes created by driver.'),
148 cfg.StrOpt(
149 'service_instance_base_boot_volume_id',
150 help="UUID of volume in Cinder, that will be used as base volume "
151 "that bootable volume clone from during service instance "
152 "creation. Only used if driver_handles_share_servers=True."),
153 cfg.BoolOpt(
154 'service_instance_boot_volume_delete_on_termination',
155 default=True,
156 help='Whether the root volume is deleted when the service instance '
157 'is terminated. Only used if driver_handles_share_servers=True.'
158 ),
159 cfg.StrOpt('service_instance_boot_volume_name_template',
160 default='manila-share-%s-boot',
161 help="Boot volume name template."),
162]
164no_share_servers_handling_mode_opts = [
165 cfg.StrOpt(
166 "service_instance_name_or_id",
167 help="Name or ID of service instance in Nova to use for share "
168 "exports. Used only when share servers handling is disabled."),
169 cfg.HostAddressOpt(
170 "service_net_name_or_ip",
171 help="Can be either name of network that is used by service "
172 "instance within Nova to get IP address or IP address itself "
173 "(either IPv4 or IPv6) for managing shares there. "
174 "Used only when share servers handling is disabled."),
175 cfg.HostAddressOpt(
176 "tenant_net_name_or_ip",
177 help="Can be either name of network that is used by service "
178 "instance within Nova to get IP address or IP address itself "
179 "(either IPv4 or IPv6) for exporting shares. "
180 "Used only when share servers handling is disabled."),
181]
183common_opts = [
184 cfg.StrOpt(
185 "service_instance_user",
186 help="User in service instance that will be used for authentication."),
187 cfg.StrOpt(
188 "service_instance_password",
189 secret=True,
190 help="Password for service instance user."),
191 cfg.StrOpt(
192 "path_to_private_key",
193 help="Path to host's private key."),
194 cfg.IntOpt(
195 "max_time_to_build_instance",
196 default=300,
197 help="Maximum time in seconds to wait for creating service instance."),
198 cfg.BoolOpt(
199 "limit_ssh_access",
200 default=False,
201 help="Block SSH connection to the service instance from other "
202 "networks than service network."),
203]
205CONF = cfg.CONF
208class ServiceInstanceManager(object):
209 """Manages nova instances for various share drivers.
211 This class provides following external methods:
213 1. set_up_service_instance: creates instance and sets up share
214 infrastructure.
215 2. ensure_service_instance: ensure service instance is available.
216 3. delete_service_instance: removes service instance and network
217 infrastructure.
218 """
219 _INSTANCE_CONNECTION_PROTO = "SSH"
221 def get_config_option(self, key):
222 """Returns value of config option.
224 :param key: key of config' option.
225 :returns: str -- value of config's option.
226 first priority is driver's config,
227 second priority is global config.
228 """
229 if self.driver_config:
230 return self.driver_config.safe_get(key)
231 return CONF.get(key)
233 def _get_network_helper(self):
234 # Historically, there were multiple types of network helper,
235 # but currently the only network helper type is Neutron.
236 return NeutronNetworkHelper(self)
238 def __init__(self, driver_config=None):
240 super(ServiceInstanceManager, self).__init__()
241 self.driver_config = driver_config
243 if self.driver_config:
244 self.driver_config.append_config_values(common_opts)
245 if self.get_config_option("driver_handles_share_servers"):
246 self.driver_config.append_config_values(
247 share_servers_handling_mode_opts)
248 else:
249 self.driver_config.append_config_values(
250 no_share_servers_handling_mode_opts)
251 else:
252 CONF.register_opts(common_opts)
253 if self.get_config_option("driver_handles_share_servers"):
254 CONF.register_opts(share_servers_handling_mode_opts)
255 else:
256 CONF.register_opts(no_share_servers_handling_mode_opts)
258 if not self.get_config_option("service_instance_user"):
259 raise exception.ServiceInstanceException(
260 _('Service instance user is not specified.'))
261 self.admin_context = context.get_admin_context()
263 self.image_api = image.API()
264 self.compute_api = compute.API()
265 self.volume_api = volume.API()
267 self.path_to_private_key = self.get_config_option(
268 "path_to_private_key")
269 self.max_time_to_build_instance = self.get_config_option(
270 "max_time_to_build_instance")
272 self.availability_zone = self.get_config_option(
273 'backend_availability_zone') or CONF.storage_availability_zone
275 if self.get_config_option("driver_handles_share_servers"):
276 self.path_to_public_key = self.get_config_option(
277 "path_to_public_key")
278 self._network_helper = None
280 @property
281 @utils.synchronized("instantiate_network_helper")
282 def network_helper(self):
283 if not self._network_helper:
284 self._network_helper = self._get_network_helper()
285 self._network_helper.setup_connectivity_with_service_instances()
286 return self._network_helper
288 def get_common_server(self):
289 data = {
290 'public_address': None,
291 'private_address': None,
292 'service_net_name_or_ip': self.get_config_option(
293 'service_net_name_or_ip'),
294 'tenant_net_name_or_ip': self.get_config_option(
295 'tenant_net_name_or_ip'),
296 }
298 data['instance'] = self.compute_api.server_get_by_name_or_id(
299 self.admin_context,
300 self.get_config_option('service_instance_name_or_id'))
302 if netutils.is_valid_ip(data['service_net_name_or_ip']):
303 data['private_address'] = [data['service_net_name_or_ip']]
304 else:
305 data['private_address'] = self._get_addresses_by_network_name(
306 data['service_net_name_or_ip'], data['instance'])
308 if netutils.is_valid_ip(data['tenant_net_name_or_ip']):
309 data['public_address'] = [data['tenant_net_name_or_ip']]
310 else:
311 data['public_address'] = self._get_addresses_by_network_name(
312 data['tenant_net_name_or_ip'], data['instance'])
314 if not (data['public_address'] and data['private_address']):
315 raise exception.ManilaException(
316 "Can not find one of net addresses for service instance. "
317 "Instance: %(instance)s, "
318 "private_address: %(private_address)s, "
319 "public_address: %(public_address)s." % data)
321 share_server = {
322 'username': self.get_config_option('service_instance_user'),
323 'password': self.get_config_option('service_instance_password'),
324 'pk_path': self.path_to_private_key,
325 'instance_id': data['instance']['id'],
326 }
327 for key in ('private_address', 'public_address'):
328 data[key + '_first'] = None
329 for address in data[key]: 329 ↛ 327line 329 didn't jump to line 327 because the loop on line 329 didn't complete
330 if netutils.is_valid_ip(address):
331 data[key + '_first'] = address
332 break
333 share_server['ip'] = data['private_address_first']
334 share_server['public_address'] = data['public_address_first']
335 return {'backend_details': share_server}
337 def _get_addresses_by_network_name(self, net_name, server):
338 net_ips = []
339 if 'networks' in server and net_name in server['networks']:
340 net_ips = server['networks'][net_name]
341 elif 'addresses' in server and net_name in server['addresses']:
342 net_ips = [addr['addr'] for addr in server['addresses'][net_name]]
343 return net_ips
345 def _get_service_instance_name(self, share_server_id):
346 """Returns service vms name."""
347 if self.driver_config and self.driver_config.config_group:
348 # Make service instance name unique for multibackend installation
349 name = "%s_%s" % (self.driver_config.config_group, share_server_id)
350 else:
351 name = share_server_id
352 return self.get_config_option("service_instance_name_template") % name
354 def _get_server_ip(self, server, net_name):
355 """Returns service IP address of service instance."""
356 net_ips = self._get_addresses_by_network_name(net_name, server)
357 if not net_ips:
358 msg = _("Failed to get service instance IP address. "
359 "Service network name is '%(net_name)s' "
360 "and provided data are '%(data)s'.")
361 msg = msg % {'net_name': net_name, 'data': str(server)}
362 raise exception.ServiceInstanceException(msg)
363 return net_ips[0]
365 def _get_or_create_security_groups(self, context, name=None,
366 description=None,
367 allow_ssh_subnet=False):
368 """Get or create security group for service_instance.
370 :param context: context, that should be used
371 :param name: this is used for selection/creation of sec.group
372 :param description: this is used on sec.group creation step only
373 :param allow_ssh_subnet: subnet details to allow ssh connection from,
374 if not supplied ssh will be allowed from any host
375 :returns: SecurityGroup -- security group instance from Nova
376 :raises: exception.ServiceInstanceException.
377 """
379 sgs = []
380 # Common security group
381 name = name or self.get_config_option(
382 "service_instance_security_group")
383 if not name:
384 LOG.warning("Name for service instance security group is not "
385 "provided. Skipping security group step.")
386 return None
387 if not description:
388 description = ("This security group is intended "
389 "to be used by share service.")
390 sec_group_data = const.SERVICE_INSTANCE_SECGROUP_DATA
391 if not allow_ssh_subnet:
392 sec_group_data += const.SSH_PORTS
394 sgs.append(self._get_or_create_security_group(name, description,
395 sec_group_data))
396 if allow_ssh_subnet:
397 if "cidr" not in allow_ssh_subnet or 'id' not in allow_ssh_subnet:
398 raise exception.ManilaException(
399 "Unable to limit SSH access")
400 ssh_sg_name = "manila-service-subnet-{}".format(
401 allow_ssh_subnet["id"])
402 sgs.append(self._get_or_create_security_group(
403 ssh_sg_name, description,
404 const.SSH_PORTS, allow_ssh_subnet["cidr"]))
405 return sgs
407 @utils.synchronized(
408 "service_instance_get_or_create_security_group", external=True)
409 def _get_or_create_security_group(self, name,
410 description, sec_group_data,
411 cidr="0.0.0.0/0"):
412 s_groups = self.network_helper.neutron_api.security_group_list({
413 "name": name,
414 })['security_groups']
415 s_groups = [s for s in s_groups if s['name'] == name]
416 if not s_groups:
417 LOG.debug("Creating security group with name '%s'.", name)
418 sg = self.network_helper.neutron_api.security_group_create(
419 name, description)['security_group']
420 for protocol, ports in sec_group_data:
421 self.network_helper.neutron_api.security_group_rule_create(
422 parent_group_id=sg['id'],
423 ip_protocol=protocol,
424 from_port=ports[0],
425 to_port=ports[1],
426 cidr=cidr,
427 )
428 elif len(s_groups) > 1:
429 msg = _("Ambiguous security_groups.")
430 raise exception.ServiceInstanceException(msg)
431 else:
432 sg = s_groups[0]
433 return sg
435 def ensure_service_instance(self, context, server):
436 """Ensures that server exists and active."""
437 if 'instance_id' not in server:
438 LOG.warning("Unable to check server existence since "
439 "'instance_id' key is not set in share server "
440 "backend details.")
441 return False
442 try:
443 inst = self.compute_api.server_get(self.admin_context,
444 server['instance_id'])
445 except exception.InstanceNotFound:
446 LOG.warning("Service instance %s does not exist.",
447 server['instance_id'])
448 return False
449 if inst['status'] == 'ACTIVE':
450 return self._check_server_availability(server)
451 return False
453 def _delete_server(self, context, server_id):
454 """Deletes the server."""
455 try:
456 self.compute_api.server_get(context, server_id)
457 except exception.InstanceNotFound:
458 LOG.debug("Service instance '%s' was not found. "
459 "Nothing to delete, skipping.", server_id)
460 return
462 self.compute_api.server_delete(context, server_id)
464 t = time.time()
465 while time.time() - t < self.max_time_to_build_instance:
466 try:
467 inst = self.compute_api.server_get(context, server_id)
468 if inst.get("status").lower() == "soft_deleted":
469 LOG.debug("Service instance '%s' was soft-deleted "
470 "successfully.", server_id)
471 break
472 except exception.InstanceNotFound:
473 LOG.debug("Service instance '%s' was deleted "
474 "successfully.", server_id)
475 break
476 time.sleep(2)
477 else:
478 raise exception.ServiceInstanceException(
479 _("Instance '%(id)s' has not been deleted in %(s)ss. "
480 "Giving up.") % {
481 'id': server_id, 's': self.max_time_to_build_instance})
483 def set_up_service_instance(self, context, network_info):
484 """Finds or creates and sets up service vm.
486 :param context: defines context, that should be used
487 :param network_info: network info for getting allocations
488 :returns: dict with service instance details
489 :raises: exception.ServiceInstanceException
490 """
491 instance_name = self._get_service_instance_name(
492 network_info['server_id'])
493 server = self._create_service_instance(
494 context, instance_name, network_info)
495 instance_details = self._get_new_instance_details(server)
497 if not self._check_server_availability(instance_details):
498 e = exception.ServiceInstanceException(
499 _('%(conn_proto)s connection has not been '
500 'established to %(server)s in %(time)ss. Giving up.') % {
501 'conn_proto': self._INSTANCE_CONNECTION_PROTO,
502 'server': server['ip'],
503 'time': self.max_time_to_build_instance})
504 e.detail_data = {'server_details': instance_details}
505 raise e
507 return instance_details
509 def _get_new_instance_details(self, server):
510 instance_details = {
511 'instance_id': server['id'],
512 'ip': server['ip'],
513 'pk_path': server.get('pk_path'),
514 'subnet_id': server.get('subnet_id'),
515 'password': self.get_config_option('service_instance_password'),
516 'username': self.get_config_option('service_instance_user'),
517 'public_address': server['public_address'],
518 }
519 if server.get('admin_ip'): 519 ↛ 521line 519 didn't jump to line 521 because the condition on line 519 was always true
520 instance_details['admin_ip'] = server['admin_ip']
521 if server.get('router_id'): 521 ↛ 523line 521 didn't jump to line 523 because the condition on line 521 was always true
522 instance_details['router_id'] = server['router_id']
523 if server.get('service_port_id'):
524 instance_details['service_port_id'] = server['service_port_id']
525 if server.get('public_port_id'):
526 instance_details['public_port_id'] = server['public_port_id']
527 if server.get('admin_port_id'): 527 ↛ 528line 527 didn't jump to line 528 because the condition on line 527 was never true
528 instance_details['admin_port_id'] = server['admin_port_id']
530 for key in ('password', 'pk_path', 'subnet_id'):
531 if not instance_details[key]:
532 instance_details.pop(key)
533 return instance_details
535 def _load_public_key(self, path):
536 with open(path, 'r') as f:
537 public_key = f.read()
538 return public_key
540 @utils.synchronized("service_instance_get_key", external=True)
541 def _get_key(self, context):
542 """Get ssh key.
544 :param context: defines context, that should be used
545 :returns: tuple with keypair name and path to private key.
546 """
547 if not (self.path_to_public_key and self.path_to_private_key):
548 return (None, None)
549 path_to_public_key = os.path.expanduser(self.path_to_public_key)
550 path_to_private_key = os.path.expanduser(self.path_to_private_key)
551 if (not os.path.exists(path_to_public_key) or
552 not os.path.exists(path_to_private_key)):
553 return (None, None)
554 keypair_name = self.get_config_option("manila_service_keypair_name")
555 keypairs = [k for k in self.compute_api.keypair_list(context)
556 if k.name == keypair_name]
557 if len(keypairs) > 1:
558 raise exception.ServiceInstanceException(_('Ambiguous keypairs.'))
560 public_key = self._load_public_key(path_to_public_key)
561 if not keypairs:
562 keypair = self.compute_api.keypair_import(
563 context, keypair_name, public_key)
564 else:
565 keypair = keypairs[0]
566 if keypair.public_key != public_key:
567 LOG.debug('Public key differs from existing keypair. '
568 'Creating new keypair.')
569 self.compute_api.keypair_delete(context, keypair.id)
570 keypair = self.compute_api.keypair_import(
571 context, keypair_name, public_key)
573 return keypair.name, path_to_private_key
575 def _get_service_image(self, context):
576 """Returns ID of service image for service vm creating."""
577 service_image_name = self.get_config_option("service_image_name")
578 images = [image.id for image in self.image_api.image_list(context)
579 if image.name == service_image_name
580 and image.status == 'active']
581 if not images:
582 raise exception.ServiceInstanceException(
583 _("Image with name '%s' was not found or is not in "
584 "'active' state.") % service_image_name)
585 if len(images) != 1:
586 raise exception.ServiceInstanceException(
587 _("Multiple 'active' state images found with name '%s'!") %
588 service_image_name)
589 return images[0]
591 def _build_bdm_from_volume(self, volume_id, delete_on_termination=True):
592 return [{
593 'boot_index': 0,
594 'uuid': volume_id,
595 'source_type': 'volume',
596 'destination_type': 'volume',
597 'delete_on_termination': bool(delete_on_termination),
598 }]
600 def _build_bdm_from_image(self, image_id, size_gb,
601 delete_on_termination=True):
602 # Nova will create the volume in Cinder and attach as root
603 return [{
604 'boot_index': 0,
605 'uuid': image_id,
606 'source_type': 'image',
607 'destination_type': 'volume',
608 'volume_size': int(size_gb),
609 'delete_on_termination': bool(delete_on_termination),
610 }]
612 def _create_service_instance(self, context, instance_name, network_info):
613 """Creates service vm and sets up networking for it."""
614 boot_from_volume = self.get_config_option(
615 'service_instance_boot_from_volume')
616 block_device_mapping_v2 = None
617 boot_volume_id = None
618 service_image_id = self._get_service_image(context)
619 if boot_from_volume:
620 del_root = self.get_config_option(
621 'service_instance_boot_volume_delete_on_termination')
622 base_vol_id = self.get_config_option(
623 "service_instance_base_boot_volume_id")
624 root_size = self.get_config_option(
625 'service_instance_boot_volume_size')
626 if base_vol_id:
627 msg = "Creating boot volume for share server '%s'."
628 LOG.debug(msg, network_info['server_id'])
629 name = self.get_config_option(
630 'service_instance_boot_volume_name_template'
631 ) % network_info[
632 'server_id']
634 volume_info = {
635 'size': root_size,
636 'name': name,
637 'description': '',
638 'availability_zone': (
639 self.availability_zone
640 ),
641 'source_volid': base_vol_id
642 }
643 vol_type = self.get_config_option(
644 "service_instance_boot_volume_type")
645 volume_info['volume_type'] = vol_type
646 volume = self.volume_api.create(context, **volume_info)
647 msg_error = _('Failed to create bootable volume')
648 timeout = self.get_config_option('max_time_to_create_volume')
649 msg_timeout = (
650 _('Volume has not been created in %ss. Giving up') %
651 timeout
652 )
654 volume = self.volume_api.wait_for_available_volume(
655 volume, timeout,
656 msg_error=msg_error, msg_timeout=msg_timeout
657 )
658 boot_volume_id = volume['id']
659 block_device_mapping_v2 = self._build_bdm_from_volume(
660 boot_volume_id, del_root
661 )
662 else:
663 block_device_mapping_v2 = self._build_bdm_from_image(
664 service_image_id,
665 root_size,
666 delete_on_termination=del_root
667 )
669 key_name, key_path = self._get_key(context)
670 if not (self.get_config_option("service_instance_password") or
671 key_name):
672 raise exception.ServiceInstanceException(
673 _('Neither service instance password nor key are available.'))
674 if not key_path: 674 ↛ 675line 674 didn't jump to line 675 because the condition on line 674 was never true
675 LOG.warning(
676 'No key path is available. May be non-existent key path is '
677 'provided. Check path_to_private_key (current value '
678 '%(private_path)s) and path_to_public_key (current value '
679 '%(public_path)s) in manila configuration file.', dict(
680 private_path=self.path_to_private_key,
681 public_path=self.path_to_public_key))
682 network_data = self.network_helper.setup_network(network_info)
683 fail_safe_data = dict(
684 router_id=network_data.get('router_id'),
685 subnet_id=network_data.get('subnet_id'))
686 if network_data.get('service_port'):
687 fail_safe_data['service_port_id'] = (
688 network_data['service_port']['id'])
689 if network_data.get('public_port'):
690 fail_safe_data['public_port_id'] = (
691 network_data['public_port']['id'])
692 if network_data.get('admin_port'):
693 fail_safe_data['admin_port_id'] = (
694 network_data['admin_port']['id'])
695 try:
696 create_kwargs = self._get_service_instance_create_kwargs()
697 if boot_from_volume:
698 create_kwargs[
699 'block_device_mapping_v2'
700 ] = block_device_mapping_v2
701 create_kwargs['image'] = None
702 else:
703 create_kwargs['image'] = service_image_id
704 service_instance = self.compute_api.server_create(
705 context,
706 name=instance_name,
707 flavor=self.get_config_option("service_instance_flavor_id"),
708 key_name=key_name,
709 nics=network_data['nics'],
710 availability_zone=self.availability_zone,
711 **create_kwargs)
713 fail_safe_data['instance_id'] = service_instance['id']
715 service_instance = self.wait_for_instance_to_be_active(
716 service_instance['id'],
717 self.max_time_to_build_instance)
719 if self.get_config_option("limit_ssh_access"): 719 ↛ 730line 719 didn't jump to line 730 because the condition on line 719 was always true
720 try:
721 service_subnet = network_data['service_subnet']
722 except KeyError:
723 LOG.error(
724 "Unable to limit ssh access to instance id: '%s'!",
725 fail_safe_data['instance_id'])
726 raise exception.ManilaException(
727 "Unable to limit SSH access - "
728 "invalid service subnet details provided")
729 else:
730 service_subnet = False
732 sec_groups = self._get_or_create_security_groups(
733 context, allow_ssh_subnet=service_subnet)
735 for sg in sec_groups:
736 sg_id = sg['id']
737 LOG.debug(
738 "Adding security group '%(sg)s' to server '%(si)s'.",
739 dict(sg=sg_id, si=service_instance["id"]))
740 self.compute_api.add_security_group_to_server(
741 context, service_instance["id"], sg_id)
743 ip = (network_data.get('service_port',
744 network_data.get(
745 'admin_port'))['fixed_ips'])
746 service_instance['ip'] = ip[0]['ip_address']
747 public_ip = (network_data.get('public_port', network_data.get(
748 'service_port'))['fixed_ips'])
749 service_instance['public_address'] = public_ip[0]['ip_address']
751 except Exception as e:
752 e.detail_data = {'server_details': fail_safe_data}
753 # Clean up boot volume if we created one and instance creation
754 # failed. If instance was created successfully, the volume is
755 # attached and will be cleaned up when the instance is deleted.
756 if boot_volume_id and 'instance_id' not in fail_safe_data: 756 ↛ 757line 756 didn't jump to line 757 because the condition on line 756 was never true
757 LOG.warning("Cleaning up orphaned boot volume %s after "
758 "service instance creation failure.",
759 boot_volume_id)
760 try:
761 self.volume_api.delete(context, boot_volume_id)
762 except Exception:
763 LOG.exception("Failed to delete orphaned boot volume %s.",
764 boot_volume_id)
765 raise
767 service_instance.update(fail_safe_data)
768 service_instance['pk_path'] = key_path
769 for pair in [('router', 'router_id'), ('service_subnet', 'subnet_id')]:
770 if pair[0] in network_data and 'id' in network_data[pair[0]]: 770 ↛ 769line 770 didn't jump to line 769 because the condition on line 770 was always true
771 service_instance[pair[1]] = network_data[pair[0]]['id']
773 admin_port = network_data.get('admin_port')
774 if admin_port: 774 ↛ 783line 774 didn't jump to line 783 because the condition on line 774 was always true
775 try:
776 service_instance['admin_ip'] = (
777 admin_port['fixed_ips'][0]['ip_address'])
778 except Exception:
779 msg = _("Admin port is being used but Admin IP was not found.")
780 LOG.exception(msg)
781 raise exception.AdminIPNotFound(reason=msg)
783 return service_instance
785 def _get_service_instance_create_kwargs(self):
786 """Specify extra arguments used when creating the service instance.
788 Classes inheriting the service instance manager can use this to easily
789 pass extra arguments such as user data or metadata.
790 """
791 return {}
793 def _check_server_availability(self, instance_details, interval=5):
794 t = time.time()
795 ssh_pool = ssh_utils.SSHPool(instance_details['ip'],
796 22,
797 interval,
798 instance_details['username'],
799 instance_details.get('password'),
800 instance_details.get('pk_path'),
801 max_size=1)
802 while time.time() - t < self.max_time_to_build_instance:
803 LOG.debug('Checking server availability.')
804 if not self._test_server_connection(instance_details, ssh_pool):
805 time.sleep(interval)
806 else:
807 return True
808 return False
810 def _test_server_connection(self, server, ssh_pool):
811 conn = None
812 try:
813 conn = ssh_pool.create(quiet=True)
814 return True
815 except Exception as e:
816 LOG.debug(e)
817 LOG.debug("Could not login to server %s over SSH. Waiting...",
818 server["ip"])
819 return False
820 finally:
821 if conn:
822 conn.close()
824 def delete_service_instance(self, context, server_details):
825 """Removes share infrastructure.
827 Deletes service vm and subnet, associated to share network.
828 """
829 instance_id = server_details.get("instance_id")
830 self._delete_server(context, instance_id)
831 self.network_helper.teardown_network(server_details)
833 def wait_for_instance_to_be_active(self, instance_id, timeout):
834 t = time.time()
835 while time.time() - t < timeout:
836 try:
837 service_instance = self.compute_api.server_get(
838 self.admin_context,
839 instance_id)
840 except exception.InstanceNotFound as e:
841 LOG.debug(e)
842 time.sleep(1)
843 continue
845 instance_status = service_instance['status']
846 # NOTE(vponomaryov): emptiness of 'networks' field checked as
847 # workaround for nova/neutron bug #1210483.
848 if (instance_status == 'ACTIVE' and
849 service_instance.get('networks', {})):
850 return service_instance
851 elif service_instance['status'] == 'ERROR':
852 break
854 LOG.debug("Waiting for instance %(instance_id)s to be active. "
855 "Current status: %(instance_status)s.",
856 dict(instance_id=instance_id,
857 instance_status=instance_status))
858 time.sleep(1)
859 raise exception.ServiceInstanceException(
860 _("Instance %(instance_id)s failed to reach active state "
861 "in %(timeout)s seconds. "
862 "Current status: %(instance_status)s.") %
863 dict(instance_id=instance_id,
864 timeout=timeout,
865 instance_status=instance_status))
867 def reboot_server(self, server, soft_reboot=False):
868 self.compute_api.server_reboot(self.admin_context,
869 server['instance_id'],
870 soft_reboot)
873class BaseNetworkhelper(metaclass=abc.ABCMeta):
875 @property
876 @abc.abstractmethod
877 def NAME(self):
878 """Returns code name of network helper."""
880 @abc.abstractmethod
881 def __init__(self, service_instance_manager):
882 """Instantiates class and its attrs."""
884 @abc.abstractmethod
885 def get_network_name(self, network_info):
886 """Returns name of network for service instance."""
888 @abc.abstractmethod
889 def setup_connectivity_with_service_instances(self):
890 """Sets up connectivity between Manila host and service instances."""
892 @abc.abstractmethod
893 def setup_network(self, network_info):
894 """Sets up network for service instance."""
896 @abc.abstractmethod
897 def teardown_network(self, server_details):
898 """Teardowns network resources provided for service instance."""
901class NeutronNetworkHelper(BaseNetworkhelper):
903 def __init__(self, service_instance_manager):
904 self.get_config_option = service_instance_manager.get_config_option
905 self.vif_driver = importutils.import_class(
906 self.get_config_option("interface_driver"))()
908 if service_instance_manager.driver_config:
909 self._network_config_group = (
910 service_instance_manager.driver_config.network_config_group or
911 service_instance_manager.driver_config.config_group)
912 else:
913 self._network_config_group = None
915 self.use_admin_port = False
916 self.use_service_network = True
917 self._neutron_api = None
918 self._service_network_id = None
919 self.connect_share_server_to_tenant_network = (
920 self.get_config_option('connect_share_server_to_tenant_network'))
922 self.admin_network_id = self.get_config_option('admin_network_id')
923 self.admin_subnet_id = self.get_config_option('admin_subnet_id')
925 if self.admin_network_id and self.admin_subnet_id: 925 ↛ 926line 925 didn't jump to line 926 because the condition on line 925 was never true
926 self.use_admin_port = True
927 if self.use_admin_port and self.connect_share_server_to_tenant_network: 927 ↛ 928line 927 didn't jump to line 928 because the condition on line 927 was never true
928 self.use_service_network = False
930 @property
931 def NAME(self):
932 return NEUTRON_NAME
934 @property
935 def admin_project_id(self):
936 return self.neutron_api.admin_project_id
938 @property
939 @utils.synchronized("instantiate_neutron_api_neutron_net_helper")
940 def neutron_api(self):
941 if not self._neutron_api:
942 self._neutron_api = neutron.API(
943 config_group_name=self._network_config_group)
944 return self._neutron_api
946 @property
947 @utils.synchronized("service_network_id_neutron_net_helper")
948 def service_network_id(self):
949 if not self._service_network_id:
950 self._service_network_id = self._get_service_network_id()
951 return self._service_network_id
953 def get_network_name(self, network_info):
954 """Returns name of network for service instance."""
955 net = self.neutron_api.get_network(network_info['neutron_net_id'])
956 return net['name']
958 @coordination.synchronized("service_instance_get_service_network")
959 def _get_service_network_id(self):
960 """Finds existing or creates new service network."""
961 service_network_name = self.get_config_option("service_network_name")
962 networks = []
963 for network in self.neutron_api.get_all_admin_project_networks():
964 if network['name'] == service_network_name: 964 ↛ 963line 964 didn't jump to line 963 because the condition on line 964 was always true
965 networks.append(network)
966 if len(networks) > 1:
967 raise exception.ServiceInstanceException(
968 _('Ambiguous service networks.'))
969 elif not networks:
970 return self.neutron_api.network_create(
971 self.admin_project_id, service_network_name)['id']
972 else:
973 return networks[0]['id']
975 @utils.synchronized(
976 "service_instance_setup_and_teardown_network_for_instance",
977 external=True)
978 def teardown_network(self, server_details):
979 subnet_id = server_details.get("subnet_id")
980 router_id = server_details.get("router_id")
982 service_port_id = server_details.get("service_port_id")
983 public_port_id = server_details.get("public_port_id")
984 admin_port_id = server_details.get("admin_port_id")
985 for port_id in (service_port_id, public_port_id, admin_port_id):
986 if port_id:
987 try:
988 self.neutron_api.delete_port(port_id)
989 except exception.NetworkException as e:
990 if e.kwargs.get('code') != 404:
991 raise
992 LOG.debug("Failed to delete port %(port_id)s with error: "
993 "\n %(exc)s", {"port_id": port_id, "exc": e})
995 if subnet_id:
996 ports = self.neutron_api.list_ports(
997 fields=['device_id', 'device_owner'],
998 fixed_ips=['subnet_id=%s' % subnet_id])
999 # NOTE(vponomaryov): iterate ports to get to know whether current
1000 # subnet is used or not. We will not remove it from router if it
1001 # is used.
1002 for port in ports:
1003 # NOTE(vponomaryov): if device_id is present, then we know that
1004 # this port is used. Also, if device owner is 'compute:*', then
1005 # we know that it is VM. We continue only if both are 'True'.
1006 if (port['device_id'] and
1007 port['device_owner'].startswith('compute:')):
1008 # NOTE(vponomaryov): There are other share servers
1009 # exist that use this subnet. So, do not remove it
1010 # from router.
1011 return
1012 if router_id:
1013 try:
1014 # NOTE(vponomaryov): there is no other share servers or
1015 # some VMs that use this subnet. So, remove it from router.
1016 self.neutron_api.router_remove_interface(
1017 router_id, subnet_id)
1018 except exception.NetworkException as e:
1019 if e.kwargs['code'] != 404:
1020 raise
1021 LOG.debug('Subnet %(subnet_id)s is not attached to the '
1022 'router %(router_id)s.',
1023 {'subnet_id': subnet_id, 'router_id': router_id})
1024 self.neutron_api.update_subnet(subnet_id, '')
1026 @utils.synchronized(
1027 "service_instance_setup_and_teardown_network_for_instance",
1028 external=True)
1029 def setup_network(self, network_info):
1030 neutron_net_id = network_info['neutron_net_id']
1031 neutron_subnet_id = network_info['neutron_subnet_id']
1032 network_data = dict()
1033 subnet_name = ('service_subnet_for_handling_of_share_server_for_'
1034 'tenant_subnet_%s' % neutron_subnet_id)
1036 if self.use_service_network:
1037 network_data['service_subnet'] = self._get_service_subnet(
1038 subnet_name)
1039 if not network_data['service_subnet']:
1040 network_data['service_subnet'] = (
1041 self.neutron_api.subnet_create(
1042 self.admin_project_id, self.service_network_id,
1043 subnet_name, self._get_cidr_for_subnet(),
1044 self.connect_share_server_to_tenant_network))
1046 network_data['ports'] = []
1048 if not self.connect_share_server_to_tenant_network:
1049 network_data['router'] = self._get_private_router(
1050 neutron_net_id, neutron_subnet_id)
1051 try:
1052 self.neutron_api.router_add_interface(
1053 network_data['router']['id'],
1054 network_data['service_subnet']['id'])
1055 except exception.NetworkException as e:
1056 if e.kwargs['code'] != 400:
1057 raise
1058 LOG.debug('Subnet %(subnet_id)s is already attached to the '
1059 'router %(router_id)s.',
1060 {'subnet_id': network_data['service_subnet']['id'],
1061 'router_id': network_data['router']['id']})
1062 else:
1063 network_data['public_port'] = self.neutron_api.create_port(
1064 self.admin_project_id, neutron_net_id,
1065 subnet_id=neutron_subnet_id, device_owner='manila')
1066 network_data['ports'].append(network_data['public_port'])
1068 if self.use_service_network:
1069 network_data['service_port'] = self.neutron_api.create_port(
1070 self.admin_project_id, self.service_network_id,
1071 subnet_id=network_data['service_subnet']['id'],
1072 device_owner='manila')
1073 network_data['ports'].append(network_data['service_port'])
1075 if self.use_admin_port:
1076 network_data['admin_port'] = self.neutron_api.create_port(
1077 self.admin_project_id, self.admin_network_id,
1078 subnet_id=self.admin_subnet_id, device_owner='manila')
1079 network_data['ports'].append(network_data['admin_port'])
1081 try:
1082 self.setup_connectivity_with_service_instances()
1083 except Exception:
1084 for port in network_data['ports']:
1085 self.neutron_api.delete_port(port['id'])
1086 raise
1088 network_data['nics'] = [
1089 {'port-id': port['id']} for port in network_data['ports']]
1090 public_ip = network_data.get(
1091 'public_port', network_data.get('service_port'))
1092 network_data['ip_address'] = public_ip['fixed_ips'][0]['ip_address']
1094 return network_data
1096 def _get_cidr_for_subnet(self):
1097 """Returns not used cidr for service subnet creating."""
1098 subnets = self._get_all_service_subnets()
1099 used_cidrs = set(subnet['cidr'] for subnet in subnets)
1100 serv_cidr = netaddr.IPNetwork(
1101 self.get_config_option("service_network_cidr"))
1102 division_mask = self.get_config_option("service_network_division_mask")
1103 for subnet in serv_cidr.subnet(division_mask):
1104 cidr = str(subnet.cidr)
1105 if cidr not in used_cidrs:
1106 return cidr
1107 else:
1108 raise exception.ServiceInstanceException(_('No available cidrs.'))
1110 def setup_connectivity_with_service_instances(self):
1111 """Sets up connectivity with service instances.
1113 Creates host port in service network and/or admin network, creating
1114 and setting up required network devices.
1115 """
1116 if self.use_service_network: 1116 ↛ 1126line 1116 didn't jump to line 1126 because the condition on line 1116 was always true
1117 LOG.debug("Plugging service instance into service network %s.",
1118 self.service_network_id)
1119 port = self._get_service_port(
1120 self.service_network_id, None, 'manila-share')
1121 port = self._add_fixed_ips_to_service_port(port)
1122 interface_name = self.vif_driver.get_device_name(port)
1123 device = ip_lib.IPDevice(interface_name)
1124 self._plug_interface_in_host(interface_name, device, port)
1126 if self.use_admin_port: 1126 ↛ exitline 1126 didn't return from function 'setup_connectivity_with_service_instances' because the condition on line 1126 was always true
1127 LOG.debug("Plugging service instance into admin network %s.",
1128 self.admin_network_id)
1129 port = self._get_service_port(
1130 self.admin_network_id, self.admin_subnet_id,
1131 'manila-admin-share')
1132 interface_name = self.vif_driver.get_device_name(port)
1133 device = ip_lib.IPDevice(interface_name)
1134 self._plug_interface_in_host(interface_name, device, port,
1135 clear_outdated_routes=True)
1137 @utils.synchronized("service_instance_plug_interface_in_host",
1138 external=True)
1139 def _plug_interface_in_host(self, interface_name, device, port,
1140 clear_outdated_routes=False):
1142 LOG.debug("Plug interface into host - interface_name: %s, "
1143 "device: %s, port: %s", interface_name, device, port)
1144 self.vif_driver.plug(interface_name, port['id'], port['mac_address'])
1145 cidrs_to_clear = []
1146 ip_cidrs = []
1147 for fixed_ip in port['fixed_ips']:
1148 subnet = self.neutron_api.get_subnet(fixed_ip['subnet_id'])
1149 if clear_outdated_routes:
1150 cidrs_to_clear.append(subnet['cidr'])
1152 net = netaddr.IPNetwork(subnet['cidr'])
1153 ip_cidr = '%s/%s' % (fixed_ip['ip_address'], net.prefixlen)
1154 ip_cidrs.append(ip_cidr)
1156 self.vif_driver.init_l3(interface_name, ip_cidrs,
1157 clear_cidrs=cidrs_to_clear)
1159 @utils.synchronized("service_instance_get_service_port", external=True)
1160 def _get_service_port(self, network_id, subnet_id, device_id):
1161 """Find or creates service neutron port.
1163 This port will be used for connectivity with service instances.
1164 """
1165 host = self.get_config_option("service_network_host") or CONF.host
1166 search_opts = {'device_id': device_id,
1167 'binding:host_id': host}
1168 ports = [port for port in self.neutron_api.
1169 list_ports(**search_opts)]
1170 if len(ports) > 1:
1171 raise exception.ServiceInstanceException(
1172 _('Error. Ambiguous service ports.'))
1173 elif not ports:
1174 port = self.neutron_api.create_port(
1175 self.admin_project_id, network_id, subnet_id=subnet_id,
1176 device_id=device_id, device_owner='manila:share', host_id=host,
1177 port_security_enabled=False)
1178 else:
1179 port = ports[0]
1180 return port
1182 @utils.synchronized(
1183 "service_instance_add_fixed_ips_to_service_port", external=True)
1184 def _add_fixed_ips_to_service_port(self, port):
1185 network = self.neutron_api.get_network(self.service_network_id)
1186 subnets = set(network['subnets'])
1187 port_fixed_ips = []
1188 for fixed_ip in port['fixed_ips']:
1189 port_fixed_ips.append({'subnet_id': fixed_ip['subnet_id'],
1190 'ip_address': fixed_ip['ip_address']})
1191 if fixed_ip['subnet_id'] in subnets: 1191 ↛ 1188line 1191 didn't jump to line 1188 because the condition on line 1191 was always true
1192 subnets.remove(fixed_ip['subnet_id'])
1194 # If there are subnets here that means that
1195 # we need to add those to the port and call update.
1196 if subnets: 1196 ↛ 1201line 1196 didn't jump to line 1201 because the condition on line 1196 was always true
1197 port_fixed_ips.extend([dict(subnet_id=s) for s in subnets])
1198 port = self.neutron_api.update_port_fixed_ips(
1199 port['id'], {'fixed_ips': port_fixed_ips})
1201 return port
1203 @utils.synchronized("service_instance_get_private_router", external=True)
1204 def _get_private_router(self, neutron_net_id, neutron_subnet_id):
1205 """Returns router attached to private subnet gateway."""
1206 private_subnet = self.neutron_api.get_subnet(neutron_subnet_id)
1207 if not private_subnet['gateway_ip']:
1208 raise exception.ServiceInstanceException(
1209 _('Subnet must have gateway.'))
1210 private_network_ports = [p for p in self.neutron_api.list_ports(
1211 network_id=neutron_net_id)]
1212 for p in private_network_ports:
1213 fixed_ip = p['fixed_ips'][0]
1214 if (fixed_ip['subnet_id'] == private_subnet['id'] and 1214 ↛ 1212line 1214 didn't jump to line 1212 because the condition on line 1214 was always true
1215 fixed_ip['ip_address'] == private_subnet['gateway_ip']):
1216 private_subnet_gateway_port = p
1217 break
1218 else:
1219 raise exception.ServiceInstanceException(
1220 _('Subnet gateway is not attached to the router.'))
1221 private_subnet_router = self.neutron_api.show_router(
1222 private_subnet_gateway_port['device_id'])
1223 return private_subnet_router
1225 @utils.synchronized("service_instance_get_service_subnet", external=True)
1226 def _get_service_subnet(self, subnet_name):
1227 all_service_subnets = self._get_all_service_subnets()
1228 service_subnets = [subnet for subnet in all_service_subnets
1229 if subnet['name'] == subnet_name]
1230 if len(service_subnets) == 1:
1231 return service_subnets[0]
1232 elif not service_subnets:
1233 unused_service_subnets = [subnet for subnet in all_service_subnets
1234 if subnet['name'] == '']
1235 if unused_service_subnets:
1236 service_subnet = unused_service_subnets[0]
1237 self.neutron_api.update_subnet(
1238 service_subnet['id'], subnet_name)
1239 return service_subnet
1240 return None
1241 else:
1242 raise exception.ServiceInstanceException(
1243 _('Ambiguous service subnets.'))
1245 @utils.synchronized(
1246 "service_instance_get_all_service_subnets", external=True)
1247 def _get_all_service_subnets(self):
1248 service_network = self.neutron_api.get_network(self.service_network_id)
1249 subnets = []
1250 for subnet_id in service_network['subnets']:
1251 subnets.append(self.neutron_api.get_subnet(subnet_id))
1252 return subnets