def rotate_lv(*, device, size, debug, forward):
    """Rotate a logical volume by a single PE.

    If forward:
        Move the first physical extent of an LV to the end
    else:
        Move the last physical extent of a LV to the start

    then poke LVM to refresh the mapping.
    """

    import augeas
    class Augeas(augeas.Augeas):
        def get_int(self, key):
            return int(self.get(key + '/int'))

        def set_int(self, key, val):
            return self.set(key + '/int', '%d' % val)

        def incr(self, key, by=1):
            orig = self.get_int(key)
            self.set_int(key, orig + by)

        def decr(self, key):
            self.incr(key, by=-1)

    lv_info = subprocess.check_output(
        'lvm lvs --noheadings --rows --units=b --nosuffix '
        '-o vg_name,vg_uuid,lv_name,lv_uuid,lv_attr --'.split()
        + [device.devpath], universal_newlines=True).splitlines()
    vgname, vg_uuid, lvname, lv_uuid, lv_attr = (fi.lstrip() for fi in lv_info)
    active = lv_attr[4] == 'a'

    # Make sure the volume isn't in use by unmapping it
    quiet_call(
        ['lvm', 'lvchange', '-an', '--', '{}/{}'.format(vgname, lvname)])

    with tempfile.TemporaryDirectory(suffix='.blocks') as tdname:
        vgcfgname = tdname + '/vg.cfg'
        print('Loading LVM metadata... ', end='', flush=True)
        quiet_call(
            ['lvm', 'vgcfgbackup', '--file', vgcfgname, '--', vgname])
        aug = Augeas(
            loadpath=pkg_resources.resource_filename('blocks', 'augeas'),
            root='/dev/null',
            flags=augeas.Augeas.NO_MODL_AUTOLOAD | augeas.Augeas.SAVE_NEWFILE)
        vgcfg = open(vgcfgname)
        vgcfg_orig = vgcfg.read()
        aug.set('/raw/vgcfg', vgcfg_orig)

        aug.text_store('LVM.lns', '/raw/vgcfg', '/vg')
        print('ok')

        # There is no easy way to quote for XPath, so whitelist
        assert all(ch in ASCII_ALNUM_WHITELIST for ch in vgname), vgname
        assert all(ch in ASCII_ALNUM_WHITELIST for ch in lvname), lvname

        aug.defvar('vg', '/vg/{}/dict'.format(vgname))
        assert aug.get('$vg/id/str') == vg_uuid
        aug.defvar('lv', '$vg/logical_volumes/dict/{}/dict'.format(lvname))
        assert aug.get('$lv/id/str') == lv_uuid

        rotate_aug(aug, forward, size)
        aug.text_retrieve('LVM.lns', '/raw/vgcfg', '/vg', '/raw/vgcfg.new')
        open(vgcfgname + '.new', 'w').write(aug.get('/raw/vgcfg.new'))
        rotate_aug(aug, not forward, size)
        aug.text_retrieve('LVM.lns', '/raw/vgcfg', '/vg', '/raw/vgcfg.backagain')
        open(vgcfgname + '.backagain', 'w').write(aug.get('/raw/vgcfg.backagain'))

        if debug:
            print('CHECK STABILITY')
            subprocess.call(
                ['git', '--no-pager', 'diff', '--no-index', '--patience', '--color-words',
                 '--', vgcfgname, vgcfgname + '.backagain'])
            if forward:
                print('CHECK CORRECTNESS (forward)')
            else:
                print('CHECK CORRECTNESS (backward)')
            subprocess.call(
                ['git', '--no-pager', 'diff', '--no-index', '--patience', '--color-words',
                 '--', vgcfgname, vgcfgname + '.new'])

        if forward:
            print(
                'Rotating the second extent to be the first... ',
                end='', flush=True)
        else:
            print(
                'Rotating the last extent to be the first... ',
                end='', flush=True)
        quiet_call(
            ['lvm', 'vgcfgrestore', '--file', vgcfgname + '.new', '--', vgname])
        # Make sure LVM updates the mapping, this is pretty critical
        quiet_call(
            ['lvm', 'lvchange', '--refresh', '--', '{}/{}'.format(vgname, lvname)])
        if active:
            quiet_call(
                ['lvm', 'lvchange', '-ay', '--', '{}/{}'.format(vgname, lvname)])
        print('ok')
Rotate a logical volume by a single PE.

    If forward:
        Move the first physical extent of an LV to the end
    else:
        Move the last physical extent of a LV to the start

    then poke LVM to refresh the mapping.