def _decrypt_image(encrypted_filename, encrypted_key, encrypted_iv, cloud_private_key, decrypted_filename): key, err = utils.execute('openssl', 'rsautl', '-decrypt', '-inkey', '%s' % cloud_private_key, process_input=encrypted_key, check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt private key: %s") % err) iv, err = utils.execute('openssl', 'rsautl', '-decrypt', '-inkey', '%s' % cloud_private_key, process_input=encrypted_iv, check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt initialization " "vector: %s") % err) _out, err = utils.execute('openssl', 'enc', '-d', '-aes-128-cbc', '-in', '%s' % (encrypted_filename,), '-K', '%s' % (key,), '-iv', '%s' % (iv,), '-out', '%s' % (decrypted_filename,), check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt image file " "%(image_file)s: %(err)s") % {'image_file': encrypted_filename, 'err': err})
def _remove_destroy(self, name, project): """ Remove the LUN from the dataset and destroy the actual LUN on the storage system. """ lun_id = self._get_lun_id(name, project) if not lun_id: raise exception.Error( _("Failed to find LUN ID for volume %s") % (name)) member = self.client.factory.create('DatasetMemberParameter') member.ObjectNameOrId = lun_id members = self.client.factory.create('ArrayOfDatasetMemberParameter') members.DatasetMemberParameter = [member] dataset_name = self._dataset_name(project) server = self.client.service lock_id = server.DatasetEditBegin(DatasetNameOrId=dataset_name) try: server.DatasetRemoveMember(EditLockId=lock_id, Destroy=True, DatasetMemberParameters=members) server.DatasetEditCommit(EditLockId=lock_id, AssumeConfirmation=True) except (suds.WebFault, Exception): server.DatasetEditRollback(EditLockId=lock_id) msg = _('Failed to remove and delete dataset member') raise exception.Error(msg)
def create_volume(self, volume): """Creates a logical volume. Can optionally return a Dictionary of changes to the volume object to be persisted.""" # For now the scheduling logic will be to try to fit the volume in # the first available backend. # TODO better scheduling once APIs are in place sm_vol_rec = None backends = self.db.sm_backend_conf_get_all(self.ctxt) for backend in backends: # Ensure that storage repo exists, if not create. # This needs to be done because if nova compute and # volume are both running on this host, then, as a # part of detach_volume, compute could potentially forget SR self._create_storage_repo(self.ctxt, backend) sm_vol_rec = self._volumeops.\ create_volume_for_sm(volume, backend['sr_uuid']) if sm_vol_rec: LOG.debug(_('Volume will be created in backend - %d') \ % backend['id']) break if sm_vol_rec: # Update db sm_vol_rec['id'] = volume['id'] sm_vol_rec['backend_id'] = backend['id'] try: self.db.sm_volume_create(self.ctxt, sm_vol_rec) except Exception as ex: LOG.exception(ex) raise exception.Error(_("Failed to update volume in db")) else: raise exception.Error(_('Unable to create volume'))
def _inner(): """Function to do the image data transfer through an update and thereon checks if the state is 'active'.""" self.glance_client.update_image(self.image_id, image_meta=self.image_meta, image_data=self.input) self._running = True while self._running: try: _get_image_meta = self.glance_client.get_image_meta image_status = _get_image_meta(self.image_id).get("status") if image_status == "active": self.stop() self.done.send(True) # If the state is killed, then raise an exception. elif image_status == "killed": self.stop() exc_msg = (_("Glance image %s is in killed state") % self.image_id) LOG.error(exc_msg) self.done.send_exception(exception.Error(exc_msg)) elif image_status in ["saving", "queued"]: greenthread.sleep(GLANCE_POLL_INTERVAL) else: self.stop() exc_msg = _("Glance image " "%(image_id)s is in unknown state " "- %(state)s") % { "image_id": self.image_id, "state": image_status} LOG.error(exc_msg) self.done.send_exception(exception.Error(exc_msg)) except Exception, exc: self.stop() self.done.send_exception(exc)
def detach_volume(self, context, volume_id, **kwargs): volume = self._get_volume(context, volume_id) instance_id = volume.get('instance_id', None) if not instance_id: raise exception.Error("Volume isn't attached to anything!") if volume['status'] == "available": raise exception.Error("Volume is already detached") try: volume.start_detach() instance = self._get_instance(context, instance_id) rpc.cast( '%s.%s' % (FLAGS.compute_topic, instance['node_name']), { "method": "detach_volume", "args": { "instance_id": instance_id, "volume_id": volume_id } }) except exception.NotFound: # If the instance doesn't exist anymore, # then we need to call detach blind volume.finish_detach() return defer.succeed({ 'attachTime': volume['attach_time'], 'device': volume['mountpoint'], 'instanceId': instance_id, 'requestId': context.request_id, 'status': volume['attach_status'], 'volumeId': volume_id })
def _calc_signature_2(self, params, verb, server_string, path): """Generate AWS signature version 2 string.""" LOG.debug('using _calc_signature_2') string_to_sign = '%s\n%s\n%s\n' % (verb, server_string, path) if 'SignatureMethod' not in params: raise exception.Error('No SignatureMethod specified') if params['SignatureMethod'] == 'HmacSHA256': if not self.hmac_256: raise exception.Error('SHA256 not supported on this server') current_hmac = self.hmac_256 elif params['SignatureMethod'] == 'HmacSHA1': current_hmac = self.hmac else: raise exception.Error('SignatureMethod %s not supported' % params['SignatureMethod']) keys = params.keys() keys.sort() pairs = [] for key in keys: val = self._get_utf8_value(params[key]) val = urllib.quote(val, safe='-_~') pairs.append(urllib.quote(key, safe='') + '=' + val) qs = '&'.join(pairs) LOG.debug('query string: %s', qs) string_to_sign += qs LOG.debug('string_to_sign: %s', string_to_sign) current_hmac.update(string_to_sign) b64 = base64.b64encode(current_hmac.digest()) LOG.debug('len(b64)=%d', len(b64)) LOG.debug('base64 encoded digest: %s', b64) return b64
def check_for_setup_error(self): """Returns an error if prerequisites aren't met""" if not (FLAGS.san_password or FLAGS.san_privatekey): raise exception.Error(_("Specify san_password or san_privatekey")) if not (FLAGS.san_ip): raise exception.Error(_("san_ip must be set"))
def ssh_execute(ssh, cmd, process_input=None, addl_env=None, check_exit_code=True): LOG.debug(_('Running cmd (SSH): %s'), ' '.join(cmd)) if addl_env: raise exception.Error(_('Environment not supported over SSH')) if process_input: # This is (probably) fixable if we need it... raise exception.Error(_('process_input not supported over SSH')) stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd) channel = stdout_stream.channel #stdin.write('process_input would go here') #stdin.flush() # NOTE(justinsb): This seems suspicious... # ...other SSH clients have buffering issues with this approach stdout = stdout_stream.read() stderr = stderr_stream.read() stdin_stream.close() exit_status = channel.recv_exit_status() # exit_status == -1 if no exit code was returned if exit_status != -1: LOG.debug(_('Result was %s') % exit_status) if check_exit_code and exit_status != 0: raise exception.ProcessExecutionError(exit_code=exit_status, stdout=stdout, stderr=stderr, cmd=' '.join(cmd)) return (stdout, stderr)
def initialize_connection(self, volume, address): try: xensm_properties = dict( self.db.sm_volume_get(self.ctxt, volume['id'])) except Exception as ex: LOG.exception(ex) raise exception.Error(_("Failed to find volume in db")) # Keep the volume id key consistent with what ISCSI driver calls it xensm_properties['volume_id'] = xensm_properties['id'] del xensm_properties['id'] try: backend_conf = self.db.\ sm_backend_conf_get(self.ctxt, xensm_properties['backend_id']) except Exception as ex: LOG.exception(ex) raise exception.Error(_("Failed to find backend in db")) params = self._convert_config_params(backend_conf['config_params']) xensm_properties['flavor_id'] = backend_conf['flavor_id'] xensm_properties['sr_uuid'] = backend_conf['sr_uuid'] xensm_properties['sr_type'] = backend_conf['sr_type'] xensm_properties.update(params) xensm_properties['introduce_sr_keys'] = self.\ _get_introduce_sr_keys(params) return {'driver_volume_type': 'xensm', 'data': xensm_properties}
def release_fixed_ip(self, context, mac, address): """Called by dhcp-bridge when ip is released.""" LOG.debug(_("Releasing IP %s"), address, context=context) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) instance_ref = fixed_ip_ref['instance'] if not instance_ref: raise exception.Error( _("IP %s released that isn't associated") % address) if instance_ref['mac_address'] != mac: inst_addr = instance_ref['mac_address'] raise exception.Error( _("IP %(address)s released from" " bad mac %(inst_addr)s vs %(mac)s") % locals()) if not fixed_ip_ref['leased']: LOG.warn(_("IP %s released that was not leased"), address, context=context) self.db.fixed_ip_update(context, fixed_ip_ref['address'], {'leased': False}) if not fixed_ip_ref['allocated']: self.db.fixed_ip_disassociate(context, address) # NOTE(vish): dhcp server isn't updated until next setup, this # means there will stale entries in the conf file # the code below will update the file if necessary if FLAGS.update_dhcp_on_disassociate: network_ref = self.db.fixed_ip_get_network(context, address) self.driver.update_dhcp(context, network_ref['id'])
def check_for_setup_error(self): """Returns an error if prerequisites aren't met""" try: (out, err) = self._execute('collie', 'cluster', 'info') if not out.startswith('running'): raise exception.Error(_("Sheepdog is not working: %s") % out) except exception.ProcessExecutionError: raise exception.Error(_("Sheepdog is not working"))
def initialize_connection(self, volume, connector): """ Do the LUN masking on the storage system so the initiator can access the LUN on the target. Also return the iSCSI properties so the initiator can find the LUN. This implementation does not call _get_iscsi_properties() to get the properties because cannot store the LUN number in the database. We only find out what the LUN number will be during this method call so we construct the properties dictionary ourselves. """ initiator_name = connector['initiator'] lun_id = volume['provider_location'] if not lun_id: msg = _("No LUN ID for volume %s") raise exception.Error(msg % volume['name']) lun = self._get_lun_details(lun_id) if not lun: msg = _('Failed to get LUN details for LUN ID %s') raise exception.Error(msg % lun_id) lun_num = self._ensure_initiator_mapped(lun.HostId, lun.LunPath, initiator_name) host = self._get_host_details(lun.HostId) if not host: msg = _('Failed to get host details for host ID %s') raise exception.Error(msg % lun.HostId) portal = self._get_target_portal_for_host(host.HostId, host.HostAddress) if not portal: msg = _('Failed to get target portal for filer: %s') raise exception.Error(msg % host.HostName) iqn = self._get_iqn_for_host(host.HostId) if not iqn: msg = _('Failed to get target IQN for filer: %s') raise exception.Error(msg % host.HostName) properties = {} properties['target_discovered'] = False (address, port) = (portal['address'], portal['port']) properties['target_portal'] = '%s:%s' % (address, port) properties['target_iqn'] = iqn properties['target_lun'] = lun_num properties['volume_id'] = volume['id'] auth = volume['provider_auth'] if auth: (auth_method, auth_username, auth_secret) = auth.split() properties['auth_method'] = auth_method properties['auth_username'] = auth_username properties['auth_password'] = auth_secret return { 'driver_volume_type': 'iscsi', 'data': properties, }
def inject_data(image, key=None, net=None, partition=None, nbd=False): """Injects a ssh key and optionally net data into a disk image. it will mount the image as a fully partitioned disk and attempt to inject into the specified partition number. If partition is not specified it mounts the image as a single partition. """ device = _link_device(image, nbd) try: if not partition is None: # create partition out, err = utils.execute('sudo', 'kpartx', '-a', device) if err: raise exception.Error(_('Failed to load partition: %s') % err) mapped_device = '/dev/mapper/%sp%s' % (device.split('/')[-1], partition) else: mapped_device = device # We can only loopback mount raw images. If the device isn't there, # it's normally because it's a .vmdk or a .vdi etc if not os.path.exists(mapped_device): raise exception.Error('Mapped device was not found (we can' ' only inject raw disk images): %s' % mapped_device) # Configure ext2fs so that it doesn't auto-check every N boots out, err = utils.execute('sudo', 'tune2fs', '-c', 0, '-i', 0, mapped_device) tmpdir = tempfile.mkdtemp() try: # mount loopback to dir out, err = utils.execute('sudo', 'mount', mapped_device, tmpdir) if err: raise exception.Error( _('Failed to mount filesystem: %s') % err) try: if key: # inject key file _inject_key_into_fs(key, tmpdir) if net: _inject_net_into_fs(net, tmpdir) finally: # unmount device utils.execute('sudo', 'umount', mapped_device) finally: # remove temporary directory utils.execute('rmdir', tmpdir) if not partition is None: # remove partitions utils.execute('sudo', 'kpartx', '-d', device) finally: _unlink_device(device, nbd)
def create_sr(self, label, params): LOG.debug(_("Creating SR %s") % label) sr_ref = VolumeHelper.create_sr(self._session, label, params) if sr_ref is None: raise exception.Error(_('Could not create SR')) sr_rec = self._session.call_xenapi("SR.get_record", sr_ref) if sr_rec is None: raise exception.Error(_('Could not retrieve SR record')) return sr_rec['uuid']
def check_for_setup_error(self): """Returns an error if prerequisites aren't met""" if not self.run_local: if not (FLAGS.san_password or FLAGS.san_privatekey): raise exception.Error( _('Specify san_password or ' 'san_privatekey')) # The san_ip must always be set, because we use it for the target if not (FLAGS.san_ip): raise exception.Error(_("san_ip must be set"))
def check_for_setup_error(self): """Returns an error if prerequisites aren't met""" try: #NOTE(francois-charlier) Since 0.24 'collie cluster info -r' # gives short output, but for compatibility reason we won't # use it and just check if 'running' is in the output. (out, err) = self._execute('collie', 'cluster', 'info') if not 'running' in out.split(): raise exception.Error(_("Sheepdog is not working: %s") % out) except exception.ProcessExecutionError: raise exception.Error(_("Sheepdog is not working"))
def delete_volume(self, volume_id): logging.debug("Deleting volume with id of: %s" % (volume_id)) vol = get_volume(volume_id) if vol['status'] == "attached": raise exception.Error("Volume is still attached") if vol['node_name'] != FLAGS.storage_name: raise exception.Error("Volume is not local to this node") vol.destroy() datastore.Redis.instance().srem('volumes', vol['volume_id']) datastore.Redis.instance().srem('volumes:%s' % (FLAGS.storage_name), vol['volume_id']) return True
def get_my_linklocal(interface): try: if_str = execute("ip", "-f", "inet6", "-o", "addr", "show", interface) condition = "\s+inet6\s+([0-9a-f:]+)/\d+\s+scope\s+link" links = [re.search(condition, x) for x in if_str[0].split('\n')] address = [w.group(1) for w in links if w is not None] if address[0] is not None: return address[0] else: raise exception.Error(_("Link Local address is not found.:%s") % if_str) except Exception as ex: raise exception.Error(_("Couldn't get Link Local IP of %(interface)s" " :%(ex)s") % locals())
def get_my_linklocal(interface): try: if_str = execute('ip', '-f', 'inet6', '-o', 'addr', 'show', interface) condition = '\s+inet6\s+([0-9a-f:]+)/\d+\s+scope\s+link' links = [re.search(condition, x) for x in if_str[0].split('\n')] address = [w.group(1) for w in links if w is not None] if address[0] is not None: return address[0] else: raise exception.Error(_('Link Local address is not found.:%s') % if_str) except Exception as ex: raise exception.Error(_("Couldn't get Link Local IP of %(interface)s" " :%(ex)s") % locals())
def get_from_path(items, path): """Returns a list of items matching the specified path. Takes an XPath-like expression e.g. prop1/prop2/prop3, and for each item in items, looks up items[prop1][prop2][prop3]. Like XPath, if any of the intermediate results are lists it will treat each list item individually. A 'None' in items or any child expressions will be ignored, this function will not throw because of None (anywhere) in items. The returned list will contain no None values. """ if path is None: raise exception.Error('Invalid mini_xpath') (first_token, sep, remainder) = path.partition('/') if first_token == '': raise exception.Error('Invalid mini_xpath') results = [] if items is None: return results if not isinstance(items, list): # Wrap single objects in a list items = [items] for item in items: if item is None: continue get_method = getattr(item, 'get', None) if get_method is None: continue child = get_method(first_token) if child is None: continue if isinstance(child, list): # Flatten intermediate lists for x in child: results.append(x) else: results.append(child) if not sep: # No more tokens return results else: return get_from_path(results, remainder)
def _poll_task(self, instance_uuid, task_ref, done): """ Poll the given task, and fires the given Deferred if we get a result. """ try: task_info = self._call_method(vim_util, "get_dynamic_property", task_ref, "Task", "info") task_name = task_info.name action = dict(instance_uuid=instance_uuid, action=task_name[0:255], error=None) if task_info.state in ['queued', 'running']: return elif task_info.state == 'success': LOG.debug( _("Task [%(task_name)s] %(task_ref)s " "status: success") % locals()) done.send("success") else: error_info = str(task_info.error.localizedMessage) action["error"] = error_info LOG.warn( _("Task [%(task_name)s] %(task_ref)s " "status: error %(error_info)s") % locals()) done.send_exception(exception.Error(error_info)) db.instance_action_create(context.get_admin_context(), action) except Exception, excep: LOG.warn(_("In vmwareapi:_poll_task, Got this error %s") % excep) done.send_exception(excep)
def _create_session(self): """Creates a session with the ESX host.""" while True: try: # Login and setup the session with the ESX host for making # API calls self.vim = self._get_vim_object() session = self.vim.Login( self.vim.get_service_content().sessionManager, userName=self._host_username, password=self._host_password) # Terminate the earlier session, if possible ( For the sake of # preserving sessions as there is a limit to the number of # sessions we can have ) if self._session_id: try: self.vim.TerminateSession( self.vim.get_service_content().sessionManager, sessionId=[self._session_id]) except Exception, excep: # This exception is something we can live with. It is # just an extra caution on our side. The session may # have been cleared. We could have made a call to # SessionIsActive, but that is an overhead because we # anyway would have to call TerminateSession. LOG.debug(excep) self._session_id = session.key return except Exception, excep: LOG.critical( _("In vmwareapi:_create_session, " "got this exception: %s") % excep) raise exception.Error(excep)
def __call__(self, req): arg_dict = req.environ['wsgiorg.routing_args'][1] action = arg_dict['action'] del arg_dict['action'] context = req.environ['openstack.context'] # allow middleware up the stack to override the params params = {} if 'openstack.params' in req.environ: params = req.environ['openstack.params'] # TODO(termie): do some basic normalization on methods method = getattr(self.service_handle, action) # NOTE(vish): make sure we have no unicode keys for py2.6. params = dict([(str(k), v) for (k, v) in params.iteritems()]) result = method(context, **params) if result is None or type(result) is str or type(result) is unicode: return result try: content_type = req.best_match_content_type() serializer = { 'application/xml': nova.api.openstack.wsgi.XMLDictSerializer(), 'application/json': nova.api.openstack.wsgi.JSONDictSerializer(), }[content_type] return serializer.serialize(result) except Exception, e: raise exception.Error( _("Returned non-serializable type: %s") % result)
def reboot(self): if not self.is_running(): raise exception.Error('trying to reboot a non-running' 'instance: %s (state: %s)' % (self.name, self.state)) logging.debug('rebooting instance %s' % self.name) self.set_state(Instance.NOSTATE, 'rebooting') yield self._conn.lookupByName(self.name).destroy() self._conn.createXML(self.toXml(), 0) d = defer.Deferred() timer = task.LoopingCall(f=None) def _wait_for_reboot(): try: self.update_state() if self.is_running(): logging.debug('rebooted instance %s' % self.name) timer.stop() d.callback(None) except Exception: self.set_state(Instance.SHUTDOWN) timer.stop() d.callback(None) timer.f = _wait_for_reboot timer.start(interval=0.5, now=True) yield d
def create_port_group(session, pg_name, vswitch_name, vlan_id=0): """ Creates a port group on the host system with the vlan tags supplied. VLAN id 0 means no vlan id association. """ client_factory = session._get_vim().client.factory add_prt_grp_spec = vm_util.get_add_vswitch_port_group_spec( client_factory, vswitch_name, pg_name, vlan_id) host_mor = session._call_method(vim_util, "get_objects", "HostSystem")[0].obj network_system_mor = session._call_method(vim_util, "get_dynamic_property", host_mor, "HostSystem", "configManager.networkSystem") LOG.debug(_("Creating Port Group with name %s on " "the ESX host") % pg_name) try: session._call_method(session._get_vim(), "AddPortGroup", network_system_mor, portgrp=add_prt_grp_spec) except error_util.VimFaultException, exc: # There can be a race condition when two instances try # adding port groups at the same time. One succeeds, then # the other one will get an exception. Since we are # concerned with the port group being created, which is done # by the other call, we can ignore the exception. if error_util.FAULT_ALREADY_EXISTS not in exc.fault_list: raise exception.Error(exc)
def _common_be_export(self, context, volume, iscsi_target): """ Common logic that asks zadara_sncfg to setup iSCSI target/lun for this volume """ (out, err) = self._execute('/var/lib/zadara/bin/zadara_sncfg', 'create_export', '--pname', volume['name'], '--tid', iscsi_target, run_as_root=True, check_exit_code=0) result_xml = ElementTree.fromstring(out) response_node = result_xml.find("Sn") if response_node is None: msg = "Malformed response from zadara_sncfg" raise exception.Error(msg) sn_ip = response_node.findtext("SnIp") sn_iqn = response_node.findtext("IqnName") model_update = {} model_update['provider_location'] = _iscsi_location( sn_ip, iscsi_target, sn_iqn) return model_update
def mount(self): """Mount a disk image, using the object attributes. The first supported means provided by the mount classes is used. True, or False is returned and the 'errors' attribute contains any diagnostics. """ if self._mounter: raise exception.Error(_('image already mounted')) if not self.mount_dir: self.mount_dir = tempfile.mkdtemp() self._mkdir = True try: for h in self.handlers: mounter_cls = self._handler_class(h) mounter = mounter_cls(image=self.image, partition=self.partition, mount_dir=self.mount_dir) if mounter.do_mount(): self._mounter = mounter break else: LOG.debug(mounter.error) self._errors.append(mounter.error) finally: if not self._mounter: self.umount() # rmdir return bool(self._mounter)
def check_for_setup_error(self): """Returns an error if prerequisites aren't met""" (stdout, stderr) = self._execute('rados', 'lspools') pools = stdout.split("\n") if not FLAGS.rbd_pool in pools: raise exception.Error(_("rbd has no pool %s") % FLAGS.rbd_pool)
def determine_is_pv(cls, session, instance_id, vdi_ref, disk_image_type, os_type): """ Determine whether the VM will use a paravirtualized kernel or if it will use hardware virtualization. 1. Glance (VHD): then we use `os_type`, raise if not set 2. Glance (DISK_RAW): use Pygrub to figure out if pv kernel is available 3. Glance (DISK): pv is assumed """ LOG.debug(_("Looking up vdi %s for PV kernel"), vdi_ref) if disk_image_type == ImageType.DISK_VHD: # 1. VHD if os_type == 'windows': is_pv = False else: is_pv = True elif disk_image_type == ImageType.DISK_RAW: # 2. RAW is_pv = with_vdi_attached_here(session, vdi_ref, True, _is_vdi_pv) elif disk_image_type == ImageType.DISK: # 3. Disk is_pv = True else: raise exception.Error( _("Unknown image format %(disk_image_type)s") % locals()) return is_pv
def _determine_is_pv_glance(cls, session, vdi_ref, disk_image_type, os_type): """ For a Glance image, determine if we need paravirtualization. The relevant scenarios are: 2. Glance (VHD): then we use `os_type`, raise if not set 3. Glance (DISK_RAW): use Pygrub to figure out if pv kernel is available 4. Glance (DISK): pv is assumed """ LOG.debug(_("Looking up vdi %s for PV kernel"), vdi_ref) if disk_image_type == ImageType.DISK_VHD: # 2. VHD if os_type == 'windows': is_pv = False else: is_pv = True elif disk_image_type == ImageType.DISK_RAW: # 3. RAW is_pv = with_vdi_attached_here(session, vdi_ref, True, _is_vdi_pv) elif disk_image_type == ImageType.DISK: # 4. Disk is_pv = True else: raise exception.Error(_("Unknown image format %(disk_image_type)s") % locals()) return is_pv