理解 OpenStack + Ceph (5):OpenStack 与 Ceph 之间的集成 [OpenStack Integration with Ceph]
理解 OpenStack + Ceph 系列文章:
(1)安装和部署
(3)Ceph 物理和逻辑结构
(4)Ceph 的基础数据结构
(6)QEMU-KVM 和 Ceph RBD 的 缓存机制总结
(8)关于Ceph PGs
1. Glance 与 Ceph RBD 集成
1.1 代码
Kilo 版本中,glance-store 代码被从 glance 代码中分离出来了,地址在 https://github.com/openstack/glance_store。
Glance 中与 Ceph 相关的配置项:
配置项 | 含义 | 默认值 |
rbd_pool | 保存rbd 卷的ceph pool 名称 | images |
rbd_user | rbd user id,仅仅在使用 cephx 认证时使用 | none。让 librados 根据 ceph 配置文件决定 |
rbd_ceph_conf | Ceph 配置文件的完整路径 | /etc/ceph/ceph.conf |
rbd_store_chunk_size | 卷会被分成对象的大小(单位为 MB) | 64 |
import rbd
- class StoreLocation(location.StoreLocation) #表示 Glance image 在 RBD 中的location,格式为 "rbd://<FSID>/<POOL>/<IMAGE>/<SNAP>"
- class ImageIterator(object) #实现从 RBD image 中读取数据块
- class Store(driver.Store): #实现将 RBD 作为Nova 镜像后端(backend)
class Store(driver.Store) 实现的主要方法:
(1)获取 image 数据:根据传入的Glance image location,获取 image 的 IO Interator
def get(self, location, offset=0, chunk_size=None, context=None):
return (ImageIterator(loc.pool, loc.image, loc.snapshot, self), self.get_size(location))
1. 根据 location,定位到 rbd image
2. 调用 image.read 方法,按照 chunk size 读取 image data
3. 将 image data 返回调用端
(2)获取 image 的 size
def get_size(self, location, context=None):
. 找到 store location:loc = location.store_location
. 使用 loc 中指定的 pool 或者配置的默认pool
. 建立 connection 和 打开 IO Context:with rbd.Image(ioctx, loc.image, snapshot=loc.snapshot) as image
. 获取 image.stat() 并获取 “size”
(3)添加 image
def add(self, image_id, image_file, image_size, context=None):
. 将 image_id 作为 rbd image name:image_name = str(image_id)
. 创建 rbd image:librbd.create(ioctx, image_name, size, order, old_format=False, features=rbd.RBD_FEATURE_LAYERING)。如果 rbd 支持 RBD_FEATURE_LAYERING 的话,创建一个 clonable snapshot:rbd://fsid/pool/image/snapshot;否则,创建一个 rbd image:rbd://image
. 调用 image.write 方法将 image_file 的内容按照 chunksize 依次写入 rbd image
. 如果要创建 snapshot 的话,调用 image.create_snap(loc.snapshot) 和 image.protect_snap(loc.snapshot) 创建snapshot
(4)删除 image
def delete(self, location, context=None):
. 根据 location 计算出 rbd image,snapshot 和 pool
. 如果它是一个 snapshot (location 是 rbd://fsid/pool/image/snapshot),则 unprotect_snap,再 remove_snap,然后删除 image;这时候有可能出错,比如存在基于该 image 的 volume。
. 如果它不是一个 snapshot (location 是 rbd://image),直接删除 rbd image。这操作也可能会出错。
1.2 使用
+------------------+----------------------------------------------------------------------------------+
| Property | Value |
+------------------+----------------------------------------------------------------------------------+
| checksum | 56730d3091a764d5f8b38feeef0bfcef |
| container_format | bare |
| created_at | 2015-09-18T10:22:49Z |
| direct_url | rbd://4387471a-ae2b-47c4-b67e-9004860d0fd0/images/71dc76da-774c- |
root@ceph1:~# rbd info images/71dc76da-774c-411f-a958-1b51816ec50f
rbd image '71dc76da-774c-411f-a958-1b51816ec50f':
size kB in objects
order ( kB objects)
block_name_prefix: rbd_data.103d2246be0e
format:
features: layering
root@ceph1:~# rbd snap ls images/71dc76da-774c-411f-a958-1b51816ec50f
SNAPID NAME SIZE
snap kB
root@ceph1:~# rbd info images/71dc76da-774c-411f-a958-1b51816ec50f@snap
rbd image '71dc76da-774c-411f-a958-1b51816ec50f':
size 40162 kB in 5 objects
order 23 (8192 kB objects)
block_name_prefix: rbd_data.103d2246be0e
format: 2
features: layering
protected: True
基于该 Glance image 创建的 Cinder volume 都是该 snapshot 的 clone:
root@ceph1:~# rbd children images/71dc76da-774c-411f-a958-1b51816ec50f@snap
volumes/volume-65dbaf38-0b9d--bba4-53f12cc906e3
volumes/volume-6868a043--4f6c-917f-bbffb1a8d21a
此时如果试着去删除该 image,则会报错:
The image cannot be deleted because it is in use through the backend store outside of Glance.
这是因为该 image 的 rbd image 的 snapshot 被使用了。
2. Cinder 与 Ceph RBD 的集成
OpenStack Cinder 组件和 Ceph RBD 集成的目的是将 Cinder 卷(volume)保存在 Ceph RBD 中。当使用 Ceph RBD 作为 Cinder 的后端存储时,你不需要单独的一个 Cinder-volume 节点.
2.1 配置项
配置项 | 含义 | 默认值 |
rbd_pool | 保存rbd 卷的ceph pool 名称 | rbd |
rbd_user | 访问 RBD 的用户的 ID,仅仅在使用 cephx 认证时使用 | none |
rbd_ceph_conf | Ceph 配置文件的完整路径 | ‘’,表示使用 librados 的默认ceph 配置文件 |
rbd_secret_uuid | rbd secret uuid | |
rbd_flatten_volume_from_snapshot | RBD Snapshot 在底层会快速复制一个元信息表,但不会产生实际的数据拷贝,因此当从 Snapshot 创建新的卷时,用户可能会期望不要依赖原来的 Snapshot,这个选项开启会在创建新卷时对原来的 Snapshot 数据进行拷贝来生成一个不依赖于源 Snapshot 的卷。 | false |
rbd_max_clone_depth |
卷克隆的最大层数,超过的话则使用 fallter。设为 0 的话,则禁止克隆。 与上面这个选项类似的原因,RBD 在支持 Cinder 的部分 API (如从 Snapshot 创建卷和克隆卷)都会使用 rbd clone 操作,但是由于 RBD 目前对于多级卷依赖的 IO 操作不好,多级依赖卷会有比较严重的性能问题。因此这里设置了一个最大克隆值来避免这个问题,一旦超出这个阀值,新的卷会自动被 flatten。 |
5 |
rbd_store_chunk_size | 每个 RBD 卷实际上就是由多个对象组成的,因此用户可以指定一个对象的大小来决定对象的数量,默认是 4 MB | 4 |
rados_connect_timeout | 连接 ceph 集群的超时时间,单位为秒。如果设为负值,则使用默认 librados 中的值 | -1 |
从这里也能看出来,
(1)Cinder 不支持单个 volume 的条带化参数设置,而只是使用了公共配置项 rbd_store_chunk_size 来指定 order。
(2)Cinder 不支持卷被附加到客户机时设置缓存模式。
2.2 代码
Cinder 使用的就是之前介绍过的 rbd phthon 模块:
import rados
import rbd
它实现了以下主要接口。
2.2.1 与 RBD 的连接
def initialize_connection(self, volume, connector):
hosts, ports = self._get_mon_addrs() #调用 args = ['ceph', 'mon', 'dump', '--format=json'] 获取 monmap,再获取 hosts 和 ports
data = {
'driver_volume_type': 'rbd',
'data': {
'name': '%s/%s' % (self.configuration.rbd_pool, volume['name']),
'hosts': hosts,
'ports': ports,
'auth_enabled': (self.configuration.rbd_user is not None),
'auth_username': self.configuration.rbd_user,
'secret_type': 'ceph',
'secret_uuid': self.configuration.rbd_secret_uuid, }
}
(2)连接到 ceph rados
client = self.rados.Rados(rados_id=self.configuration.rbd_user, conffile=self.configuration.rbd_ceph_conf)
client.connect(timeout= self.configuration.rados_connect_timeout)
ioctx = client.open_ioctx(pool)
(3)断开连接
ioctx.close()
client.shutdown()
2.2.2 创建卷
with RADOSClient(self) as client:
self.rbd.RBD().create(client.ioctx,
encodeutils.safe_encode(volume['name']),
size,
order,
old_format=old_format,
features=features)
2.2.3 克隆卷
#创建克隆卷
def create_cloned_volume(self, volume, src_vref): # 因为 RBD 的 clone 方法是基于 snapshot 的,所有 cinder 会首先创建一个 snapshot,再创建一个 clone。 if CONF.rbd_max_clone_depth <= 0: #如果设置的 rbd_max_clone_depth 为负数,则做一个完整的 rbd image copy
vol.copy(vol.ioctx, dest_name)
depth = self._get_clone_depth(client, src_name) #判断 volume 对应的 image 的 clone depth,如果已经达到 CONF.rbd_max_clone_depth,则需要做 flattern
src_volume = self.rbd.Image(client.ioctx, src_name) #获取 source volume 对应的 rbd image
#如果需要 flattern,
_pool, parent, snap = self._get_clone_info(src_volume, src_name) #获取 parent 和 snapshot
src_volume.flatten() # 将 parent 的data 拷贝到该 clone 中
parent_volume = self.rbd.Image(client.ioctx, parent) #获取 paraent image
parent_volume.unprotect_snap(snap) #将 snap 去保护
parent_volume.remove_snap(snap) #删除 snapshot
src_volume.create_snap(clone_snap) #创建新的 snapshot
src_volume.protect_snap(clone_snap) #将 snapshot 加保护
self.rbd.RBD().clone(client.ioctx, src_name, clone_snap, client.ioctx, dest_name, features=client.features) #在 snapshot 上做clone
self._resize(volume) #如果 clone 的size 和 src volume 的size 不一样,则 resize
3. Nova 与 Ceph 的集成
3.1 Nova 与 Ceph 集成的配置项
配置项 | 含义 | 默认值 |
images_type |
其值可以设为下面几个选项中的一个:
|
default |
images_rbd_pool | 存放 vm 镜像文件的 RBD pool | rbd |
images_rbd_ceph_conf | Ceph 配置文件的完整路径 | ‘’ |
hw_disk_discard |
设置使用或者不使用discard 模式,使用的话需要 Need Libvirt(1.0.6)、 Qemu1.5 (raw format) 和 Qemu1.6(qcow2 format)') 的支持 "unmap" : Discard requests("trim" or "unmap") are passed to the filesystem. |
none |
rbd_user | rbd user ID | |
rbd_secret_uuid | rbd secret UUID |
- 设备直接暴露给客户机(device pass-through to directly expose physical storage devices to guests)
- 更好的性能(better performance and support for true SCSI device)
- 标准的设备命名方法(common and standard device naming identical to the physical world thus virtualising physical applications is made easier)
- 更好的扩展性(better scalability of the storage where virtual machines can attach more device (more LUNs etc…))
要让虚机使用该接口,需要设置 Glance image 的属性:
$ glance image-update --property hw_scsi_model=virtio-scsi --property hw_disk_bus=scsi
(2)在 nova.conf 中配置 hw_disk_discard = unmap
注意目前 cinder 尚不支持 discard。
3.2 Nova 中实现的 RBD image 操作
Nova 在 \nova\virt\libvirt\imagebackend.py 文件中添加了支持 RBD 的新类 class Rbd(Image) 来支持将虚机的image 放在 RBD 中。其主要方法包括:
def create_image(self, prepare_template, base, size, *args, **kwargs):
# 调用 'rbd', 'import' 命令将 image file 的数据保存到 rbd image 中
import [–image-format format-id] [–order bits] [–stripe-unit size-in-B/K/M –stripe-count num] [–image-feature feature-name]... [–image-shared] src-path[image-spec]
Creates a new image and imports its data from path (use - for stdin). The import operation will try to create sparse rbd images if possible. For import from stdin, the sparsification unit is the data block size of the destination image ( << order).
The –stripe-unit and –stripe-count arguments are optional, but must be used together.
虚机创建成功后,在 RBD 中查看创建出来的 image:
root@ceph1:~# rbd ls vms
74cbdb41--4eae-b22e-5085de8caba8_disk.local root@ceph1:~# rbd info vms/74cbdb41--4eae-b22e-5085de8caba8_disk.local
rbd image '74cbdb41-3789-4eae-b22e-5085de8caba8_disk.local':
size MB in objects
order ( kB objects)
block_name_prefix: rbd_data.11552ae8944a
format:
features: layering
查看虚机的 xml 定义文件,能看到虚机的系统盘、临时盘和交换盘的镜像文件都在 RBD 中,而且可以使用特定的 cache 模式(由 CONF.libvirt.disk_cachemodes 配置项指定)和 discard 模式(由 CONF.libvirt.hw_disk_discard 配置项设置):
<devices>
<disk type="network" device="disk">
<driver type="raw" cache="writeback" discard="unmap"/>
<source protocol="rbd" name="vms/74cbdb41-3789-4eae-b22e-5085de8caba8_disk.local">
<host name="9.115.251.194" port=""/>
<host name="9.115.251.195" port=""/>
<host name="9.115.251.218" port=""/>
</source>
<auth username="cinder">
<secret type="ceph" uuid="e21a123a-31f8-425a-86db-7204c33a6161"/>
</auth>
<target bus="virtio" dev="vdb"/>
</disk>
<disk type="network" device="disk">
<driver name="qemu" type="raw" cache="writeback"/>
<source protocol="rbd" name="volumes/volume-6868a043-1412-4f6c-917f-bbffb1a8d21a">
<host name="9.115.251.194" port=""/>
<host name="9.115.251.195" port=""/>
<host name="9.115.251.218" port=""/>
</source>
<auth username="cinder">
<secret type="ceph" uuid="e21a123a-31f8-425a-86db-7204c33a6161"/>
</auth>
<target bus="virtio" dev="vda"/>
<serial>6868a043--4f6c-917f-bbffb1a8d21a</serial>
</disk>
关于 libvirt.disk_cachemodes 配置项,可以指定镜像文件的缓存模式,其值的格式为 ”A=B",其中:
- A 可以为 'file','block','network','mount'。其中,file 是针对 file-backend 的disk(比如使用 qcow2 格式的镜像文件),block 是针对块设备的disk(比如使用 cinder volume 或者 lvm),network 是针对通过网络连接的设备的disk(比如 Ceph)。
- B 可以为 none,writethrough,writeback,directsync,unsafe。writethrough 和 writeback 可以参考 理解 OpenStack + Ceph (2):Ceph 的物理和逻辑结构,其它的请自行google。
- 比如,disk_cachemodes="network=writeback",disk_cachemodes="block=writeback"。
上面的虚机 XML 定义文件是在Nova 配置为 disk_cachemodes="network=writeback" 和 hw_disk_discard = unmap 的情形下生成的。
(2)image clone API
def clone(self, context, image_id_or_uri):
. 通过 Glance API 获取 image 的 rbd location
. 检查 rbd image 是否可以被克隆(检查它是不是在本 ceph cluster 内、是不是 raw 格式、是不是可以访问等)
. 调用 rbd 的 clone 方法来创建 clone
3.3 镜像 clone 而非下载
使用传统存储作为 image 的后端存储时,在创建虚机的过程中的创建镜像文件时,都是调用 _try_fetch_image_cache 方法来从 Glance 中将镜像文件下载到本地(第一次会缓存,以后就直接读缓存而不用下载),然后再创建镜像文件的方法。而在使用 RBD 作为镜像的后端存储时,如果 Glance 镜像文件被保存在 RBD 中,那么该过程将是重复的(先通过 Glance 从 RDB 中倒出镜像,然后再由 Nova 放到RBD中),而且是非常耗时的。针对这种情况,Nova 实现了一种新的办法,具体见下面的蓝色字体部分:
def _create_image(self, context, instance,
disk_mapping, suffix='',
disk_images=None, network_info=None,
block_device_info=None, files=None,
admin_pass=None, inject_files=True,
fallback_from_host=None): if not booted_from_volume: #如果不是从 volume 启动虚机
root_fname = imagecache.get_cache_fname(disk_images, 'image_id')
size = instance.root_gb * units.Gi
backend = image('disk')
if backend.SUPPORTS_CLONE: #如果 image backend 支持 clone 的话(目前的各种 image backend,只有 RBD 支持 clone)
def clone_fallback_to_fetch(*args, **kwargs):
backend.clone(context, disk_images['image_id']) #直接调用 backend 的 clone 函数做 image clone
fetch_func = clone_fallback_to_fetch
else:
fetch_func = libvirt_utils.fetch_image #否则走常规的 image 下载-导入过程
self._try_fetch_image_cache(backend, fetch_func, context, root_fname, disk_images['image_id'], instance, size, fallback_from_host)
可见,当 nova 后端使用 ceph 时,nova driver 调用 RBD imagebackend 命令,直接在 ceph 存储层完成镜像拷贝动作(无需消耗太多的nova性能,也无需将镜像下载到hypervisor本地,再上传镜像到ceph),如此创建虚拟机时间将会大大提升。当然,这个的前提是 image 也是保存在 ceph 中,而且 image 的格式为 raw,否则 clone 过程会报错。 具体过程如下:
(1)这是 Glance image 对应的 rbd image:
root@ceph1:~# rbd info images/0a64fa67-3e34-42e7-b7b0-423c11850e18
rbd image '0a64fa67-3e34-42e7-b7b0-423c11850e18':
size MB in objects
order ( kB objects)
block_name_prefix: rbd_data.16d21e1d755b
format:
features: layering
(2)使用该 image 创建第一个虚机
root@ceph1:~# rbd info vms/982b8eac-6bcc-4a21-bd04-b67e26188be0_disk
rbd image '982b8eac-6bcc-4a21-bd04-b67e26188be0_disk':
size MB in objects
order ( kB objects)
block_name_prefix: rbd_data.130a36a6b435
format:
features: layering
parent: images/0a64fa67-3e34-42e7-b7b0-423c11850e18@snap
overlap: MB root@ceph1:~# rbd info vms/982b8eac-6bcc-4a21-bd04-b67e26188be0_disk.local
rbd image '982b8eac-6bcc-4a21-bd04-b67e26188be0_disk.local':
size MB in objects
order ( kB objects)
block_name_prefix: rbd_data.a69b2ae8944a
format:
features: layering root@ceph1:~# rbd info vms/982b8eac-6bcc-4a21-bd04-b67e26188be0_disk.swap
rbd image '982b8eac-6bcc-4a21-bd04-b67e26188be0_disk.swap':
size kB in objects
order ( kB objects)
block_name_prefix: rbd_data.a69e74b0dc51
format:
features: layering
(3)创建第二个虚机
root@ceph1:~# rbd info vms/a9670d9a-8aa7-49ba-baf5-9d7a450172f3_disk
rbd image 'a9670d9a-8aa7-49ba-baf5-9d7a450172f3_disk':
size MB in objects
order ( kB objects)
block_name_prefix: rbd_data.13611f6abac6
format:
features: layering
parent: images/0a64fa67-3e34-42e7-b7b0-423c11850e18@snap
overlap: MB
(4)会看到 Glance image 对应的 RBD image 有两个克隆,分别是上面虚机的系统盘
root@ceph1:~# rbd children images/0a64fa67-3e34-42e7-b7b0-423c11850e18@snap
vms/982b8eac-6bcc-4a21-bd04-b67e26188be0_disk
vms/a9670d9a-8aa7-49ba-baf5-9d7a450172f3_disk
4. 其它集成
除了上面所描述的 Cinder、Nova 和 Glance 与 Ceph RBD 的集成外,OpenStack 和 Ceph 之间还有其它的集成点:
(1)使用 Ceph 替代 Swift 作为对象存储 (网络上有很多比较 Ceph 和 Swift 的文章,比如 1,2,3,)
(2)CephFS 作为 Manila 的后端(backend)
(3)Keystone 和 Ceph Object Gateway 的集成,具体可以参考文章 (1)(2)
参考文档:
http://www.sebastien-han.fr/blog/2015/02/02/openstack-and-ceph-rbd-discard/
理解 OpenStack + Ceph (5):OpenStack 与 Ceph 之间的集成 [OpenStack Integration with Ceph]的更多相关文章
- 理解 OpenStack 高可用(HA) (4): Pacemaker 和 OpenStack Resource Agent (RA)
本系列会分析OpenStack 的高可用性(HA)概念和解决方案: (1)OpenStack 高可用方案概述 (2)Neutron L3 Agent HA - VRRP (虚拟路由冗余协议) (3)N ...
- 几个 Ceph 性能优化的新方法和思路(2015 SH Ceph Day 参后感)
一周前,由 Intel 与 Redhat 在10月18日联合举办了 Shanghai Ceph Day.在这次会议上,多位专家做了十几场非常精彩的演讲.本文就这些演讲中提到的 Ceph性能优化方面的知 ...
- Percona 开始尝试基于Ceph做上层感知的分布式 MySQL 集群,使用 Ceph 提供的快照,备份和 HA 功能来解决分布式数据库的底层存储问题
本文由 Ceph 中国社区 -QiYu 翻译 英文出处:Using Ceph with MySQL 欢迎加入CCTG Over the last year, the Ceph world drew m ...
- OpenStack Austin 峰会观察:OpenStack as IaaS 已是过去,Solutions on OpenStack 才是未来
虽然搞 OpenStack 前后也有几年,但是今年在美国 Austin 举办的 OpenStack Summit 我还是第一次参加.回来之后,一直还在回味,觉得要写点东西,将我在这次峰会上的观察和思考 ...
- Openstack中keystone与外部LDAP Server的集成
openstack中keystone鉴权的用户user和password信息,通常保存在mysql数据库的keystone库: 表local_user和表password: keystone也支持外部 ...
- OpenStack、KVM、Docker——Docker之后还需要OpenStack吗?
原文链接:http://news.csdn.net/article_preview.html?preview=1&reload=1&arcid=2823129 Docker从一个新兴的 ...
- 如何理解Nginx, WSGI, Flask(Django)之间的关系
如何理解Nginx, WSGI, Flask(Django)之间的关系 值得指出的是,WSGI 是一种协议,需要区分几个相近的名词: uwsgi 同 wsgi 一样也是一种协议,uWSGI服务器正是使 ...
- WCF技术剖析之七:如何实现WCF与EnterLib PIAB、Unity之间的集成
原文:WCF技术剖析之七:如何实现WCF与EnterLib PIAB.Unity之间的集成 在这之前,我写过深入介绍MS EnterLib PIAB的文章(参阅<MS Enterprise Li ...
- openstack(Pike 版)集群部署(八)--- 连接Ceph Cluster 作为后端存储
一.openstack Glance + ceph Cluster 部署: 博客:http://www.cnblogs.com/weijie0717/p/8563294.html 参考 续 部分. ...
随机推荐
- 一:【nopcommerce系列】Nop整体架构的简单介绍,在看nop代码之前,你需要懂哪些东西
首先,我看的是Nop 3.80,最新版 百度资料很多,Nop用到的主要的技术有: 1.Mvc,最新版用的是 5.2.3.0 2.entity framework 3.autofac 4.插件化 5.( ...
- 使用CTE解决复杂查询的问题
最近,同事需要从数个表中查询用户的业务和报告数据,写了一个SQL语句,查询比较慢: Select S.Name, S.AccountantCode, ( Select COUNT(*) from ( ...
- java集合-集合大家族
在编写 Java 程序中,我们最常用的除了八种基本数据类型,String 对象外还有一个集合类,在我们的的程序中到处充斥着集合类的身影!Java 中集合大家族的成员实在是太丰富了,有常用的 Array ...
- SequenceInputStream
SequenceInputStream从名字上看, 他是一个序列字节输入流 既然是个序列 那么意味着 SequenceInputStream装着许多的输入流 所以 可以用他来合并文件 Sequence ...
- css权重
1.行内样式,指的是html文档中定义的style <h1 style="color: #fff;">header</h1> 2.ID选择器 3.类,属性选 ...
- WindowsForm如何实现类似微软project软件的甘特图?
在管理软件研发过程中,特别是涉及项目管理或者生产计划方面,都需要一款类似微软project的控件对项目下的分解任务进行图形展示(甘特图).下面介绍一下在WindowsForm下如何实现类似微软proj ...
- react实例之todo,做一个实时响应的列表操作
react实例之todo, 做一个实时响应的列表操作 在所有的mvc框架中,最常见的例子不是hello world,而是todo,由于reactjs的简单性,在不引用flux和redux的情况下,我们 ...
- Mysql一些复杂的语句
1.查找重复的行 SELECT * FROM blog_user_relation a WHERE (a.account_instance_id,a.follow_account_instance_i ...
- sharepoint2010问卷调查(2)-实现问卷的图片调查(采用自定义字段类型)
1. 首先建立个图片库上传图片 并建立文件夹1和2,1下有1.1文件夹,2下2.1文件夹,2.1下有文件夹2.1.1. 在1文件夹下放如下图片: 2.建立自定义字段类型,如下图: 3.部署后建立栏目的 ...
- 基于SharePoint 2013的论坛解决方案[开源]
前言 这是自己在空闲时间里,为了提高对SharePoint的认识和熟悉技术,做的一个Demo.可能不尽完善,但是基本功能都已经有了,欢迎大家评论和提意见.自己也会在把源代码放到Github上进行开源, ...