diff --git a/container/scheduler/my-start b/container/scheduler/my-start index b55a31cce58393b79b5ab552fafeed14417a80a4..81fabb092c0e3210ad1ca090f30e710568851b6a 100755 --- a/container/scheduler/my-start +++ b/container/scheduler/my-start @@ -82,6 +82,7 @@ cmd = %W[ -v /srv/result:/srv/result -v /srv/initrd:/srv/initrd:ro -v /srv/upload-files:/srv/upload-files:rw + -v /srv/cci/user-files:/srv/cci/user-files:rw -v /srv/os:/srv/os:ro --log-opt mode=non-blocking --log-opt max-buffer-size=4m diff --git a/doc/job/submit/upload-file.md b/doc/job/submit/upload-file.md new file mode 100644 index 0000000000000000000000000000000000000000..fd7565e7d5bdc68f3e4dcb4b2e473637e8c83065 --- /dev/null +++ b/doc/job/submit/upload-file.md @@ -0,0 +1,169 @@ +## 介绍 + +在某些测试脚本中,常常会需要使用到各种各样的配置。Linux 内核的配置项,一些需要编译的软件的配置等等,配置比较复杂,往往都需要一个配置文件来描述。所以需要一个通用的功能,支持从客户端提交任务时上传需要使用的配置文件到调度器。 + +## 使用介绍 + +目前上传文件的默认支持 pkgbuild 以及 ss 字段。下面以 pkgbuild 为例。 + +假设我们现在有一个 linux 的配置文件, 路径为本地的 /root/config.5.10-xx: +``` +... +CONFIG_LOCK_DEBUGGING_SUPPORT=y +CONFIG_ARCH_USE_CMPXCHG_LOCKREF=y +CONFIG_HAS_DMA=y +CONFIG_HAVE_CONTEXT_TRACKING=y +CONFIG_SERIO=y +CONFIG_OF_GPIO=y +... +``` +我们想要使用这个配置文件,并使用 pkgbuild 来构建内核。提交 pkgbuild job: +``` +submit build-linux.yaml config=/root/config.5.10-xx +``` + +好了,无需其他操作,程序会自动处理并上传到服务器,并使用对应的 config。 + +> pkgbuild 要使用上传好的文件,需要对应的脚本来做适配。 + +## 设计逻辑 + +### 总体流程 + + —> 1.客户端第一次提交, job.yaml 中携带了支持的文件上传字段 + + - pkgbuild 的 config 字段: + ``` + suite: pkgbuild + config: config.5.10-xxx + ``` + - ss 字段下,ss.\*.config\* 规则的字段: + ``` + ... + ss: + git: + .... + configx: config.5.10-xxx + .... + mysql: + configxx: config.5.10-xxx + ... + ``` + + —> 2.调度器处理job,包含三个步骤: + + - 如果 job 存在字段 upload_fields,处理 upload_fields 中的文件信息,保存到服务器 + - 如果 job 不存在字段 upload_fields,检查 job 是否需要上传文件。 + - 这个文件已在服务器,不做处理, + - 不在服务器,返回需要上传的字段详情,通知客户端重新提交并携带文件信息。 + - 最后一步,将文件最终上传的 url(initrd 链接)保存到变量 "upload_file_url", 以供测试脚本所读取下载 + + +### 调度器方面: + +第一阶段:处理客户端 job 中携带的 upload_fields(包含文件信息),这应该是一个列表,列表中的每一项都代表需要上传的文件信息。处理函数 **`process_upload_fields` ,** 位于 $CCI_SRC/src/lib/job.cr . + +- 寻找 job 中是否有 upload_fields 字段,如果有,进行下一步。 +- 依次迭代 upload_fields 中的每一项,执行 store_upload_file 函数保存文件到服务器。保存目录的结构为:/srv/cci/user-files/$suite/$field_name/$filename + > 对于pkgbuild类型的 job,目录结构则为:/srv/cci/user-files/$suite/$pkg_name/$field_name/$filename +- 执行 reset_upload_field 函数,删除 upload_fields 每一项中的 content 字段,添加 save_dir 字段保留文件信息。 + +下面是一个示例: + +```yaml +# 调度器保存文件执行前--------------------- +upload_fields: + - md5: 8283b295ef0d03af318faa2ed2c5d5c8 + field_name: ss.linux.config + file_name: kconfig-xx + content: |-xxxxx + +# 调度器保存文件执行后--------------------- +upload_fields: + - md5: 8283b295ef0d0123218faa2ed2c5d5c8 + field_name: ss.linux.config + file_name: kconfig-xx + save_dir: /srv/cci/user-files/..... +``` + +第二阶段:检查 job 中是否有符合要求的文件上传字段,并从里面获取 upload_fields (只包含上传文件的字段,需要返回给客户端处理) 。目前符合要求的上传字段为 ss.*.config* 以及 config (suite: build-pkg) 字段。 + +处理的核心函数 `generate_upload_fields` , 位于 $CCI_SRC/src/lib/job.cr。 + +检查 job 是否有 ss.*.config* 以及 config (suite: build-pkg) 字段。 + +> $CCI_SRC/src/lib/user_uploadfiles_fields_config.yaml 可以配置符合规则的job和字段来支持上传文件。 + +如果有,获取字段值(filepath),构建文件路径:/srv/cci/user-files/$suite/$field_name/$filename。 + > 对于pkgbuild类型的 job,目录结构则为:/srv/cci/user-files/$suite/$pkg_name/$field_name/$filename + +- 检查上述的文件路径是否存在,如果存在,export 变量 "upload_file_url", 值为文件的url, 以供测试机下载和载入。 +> http://$INITRD_HTTP_HOST:$INITRD_HTTP_PORT/cci/user-files/pkgbuild/linux/config/config.5.10-xx +- 如果不存在,添加到 upload_fields 列表,每一项为需要上传文件的字段,如 ss.linux.config ,返回给客户端,通知客户端上传该字段的文件内容: + +```yaml + { + "errcode": "RETRY_UPLOAD", + "upload_fields: $upload_fields + } +``` + +### 客户端方面: + +客户端应该要经历两个阶段。 + +第一个阶段:提交 job ,解析调度器返回的消息,如解析到 ”errcode“ == ”RETRY_UPLOAD“, 代表调度器通知客户端需要上传内容。 + +解析返回消息中 upload_fields 的内容,并构造新的 upload_fields, 里面包含了调度器保存文件所需的各种信息(md5,field,filename,content)。 + +具体函数内容在 $LKP_SRC/lib/upload_field_pack.rb ,核心为 `pack` 函数: + +- 迭代从调度器返回的 upload_fields ,对每个 upload_field ,获取对应的字段值,打包 md5, content, filename 字段。假设某个 upload_field 为 `ss.linux.config` 。 + - 获取 ss.linux.config 字段的值,如 kconfig-xx,应该是一个文件。检查 kconfig-xx 是否存在(本地目录)。 + - 不存在,raise 异常,上传出错。 + - 存在,执行 `generate_upload_field_hash` 函数, 获取 kconfig-xx 文件的 md5、文件名filename(basename) 、内容 content,填充到新的 upload_fields 中的一项。 + +```yaml +# 填充前--------------------- +upload_fields: + - ss.linux.config + +# 填充后--------------------- +upload_fields: + - md5: 8283b295ef0d0123218faa2ed2c5d5c8 + field_name: ss.linux.config + file_name: kconfig-xx + content: xxxxxx +``` + +第二步:客户端再次提交此 job ,此时携带了 upload_fields, 包含了需要上传的文件信息。 + +### 对于 ss 字段中 pkgbuild的特殊处理: + +由于 ss 字段中的 pkgbuild 任务会在服务器提交,那么对于我们的文件上传流程,需要做一些处理,默认的流程为: + + —> 1.客户端第一次提交, 有 ss.linux.config 字段 + + —> 2.调度器检查到 /srv/cci/user-files/$suite/ss.linux.config/$filename 无文件,返回,通知上传文件 + + —> 3.客户端第二次提交,携带上传的文件信息。 + + —> 4.调度器保存文件到 /srv/cci/user-files/$suite/ss.linux.config/$filename ,提交 ss 中的 pkgbuild + + —> 5.调度器接收到 pkgbuild 任务,检查 /srv/cci/user-files/build-pkg(pkgbuild)/$pkg_name/config/$filename 文件不存在,通知客户端上传文件。 + +可以看到,最后一步又通知了一遍客户端上传文件,实际我们已经上传好了文件到 $suite/ss.linux.config/$filename, 这样容易出现异常。 + +那么现在需要的就是在 ss 的 pkgbuild 任务提交前,关联 /srv/cci/user-files/$suite/ss.linux.config/$filename 到 /srv/cci/user-files/$suite/build-pkg(pkgbuild)/$pkg_name/$filename。这样在 ss 的 pkgbuild job 提交时,就能检测到对应目录文件的存在性,无需重复上传。 + +修改上述流程的第四步: + + —> 4. 调度器保存文件到 /srv/cci/user-files/$suite/ss.linux.config/$filename。 + 关联 /srv/cci/user-files/$suite/ss.linux.config/$filename 到 /srv/cci/user-files/$suite/build-pkg(pkgbuild)/$pkg_name/$filename。最后提交 ss 的 pkgbuild。 + + +## FAQ + +- This file not found in server, so we need upload it, but we not found in local..... + > 这个文件不在服务器,我们需要上传,但是在本地找不到指定的文件,建议最好指定文件的绝对路径 + diff --git a/src/lib/job.cr b/src/lib/job.cr index 868ee5475f4d30db99061c8feb811ec08cbe5923..73bf46d327317afd64cae9ecdf40bcaa877bc546 100644 --- a/src/lib/job.cr +++ b/src/lib/job.cr @@ -985,4 +985,149 @@ class Job @hash.delete("ipmi_ip") @hash.delete("serial_number") end + private def get_user_uploadfiles_fields_from_config + user_uploadfiles_fields_config = "#{ENV["CCI_SRC"]}/src/lib/user_uploadfiles_fields_config.yaml" + yaml_any_array = YAML.parse(File.read(user_uploadfiles_fields_config)).as_a + return yaml_any_array + end + + private def check_config_integrity(md5, dest_config_file) + dest_config_content_md5 = Digest::MD5.hexdigest(File.read dest_config_file) + raise "check pkg integrity failed." if md5 != dest_config_content_md5 + end + + private def get_dest_dir(field_name) + # + # pkgbuild/build-pkg:$suite/pkg_name/field_name/filename + # ss(field_name=ss.*.config*): $suite/ss.*.config*/filename + # other: $suite/field_name/filename + if (field_name =~ /ss\..*\.config.*/) || + @hash["suite"].as_s != "build-pkg" && @hash["suite"].as_s != "pkgbuild" + dest_dir = "#{SRV_USER_FILE_UPLOAD}/#{@hash["suite"].to_s}/#{field_name}" + else + _pkgbuild_repo = @hash["pkgbuild_repo"].as_s + pkg_name = _pkgbuild_repo.chomp.split('/', remove_empty: true)[-1] + dest_dir = "#{SRV_USER_FILE_UPLOAD}/#{@hash["suite"].as_s}/#{pkg_name}/#{field_name}" + end + return dest_dir + end + + private def generate_upload_fields(field_config) + uploaded_file_path_hash = Hash(String, String).new + upload_fields = [] of String + ss = Hash(String, JSON::Any).new + #process upload file field from ss.*.config* + ss = @hash["ss"]?.not_nil!.as_h if @hash.has_key?("ss") + ss.each do |pkg_name, pkg_params| + params = pkg_params == nil ? next : pkg_params.as_h + params.keys().each do |key| + if key =~ /config.*/ && params[key] != nil + field_name = "ss.#{pkg_name}.#{key}" + filename = File.basename(params[key].to_s.chomp) + dest_file_path = "#{SRV_USER_FILE_UPLOAD}/#{@hash["suite"].as_s}/#{field_name}/#{filename}" + if File.exists?(dest_file_path) + uploaded_file_path_hash[field_name] = dest_file_path + else + upload_fields << field_name + end + end + end + end + + #process upload file field from #{ENV["CCI_SRC"]}/src/lib/user_uploadfiles_fields_config.yaml + field_config.each do |field_obj| + field_hash = field_obj.as_h + if !field_hash.has_key?("suite") && !field_hash.has_key?("field_name") + raise "#{ENV["CCI_SRC"]}/src/lib/user_uploadfiles_fields_config.yaml content format error!! " + end + _suite = field_hash["suite"].as_s? + field_name = field_hash["field_name"].as_s + if _suite + next if _suite != @hash["suite"].as_s || !@hash.has_key?(field_name) + filename = File.basename(@hash[field_name].to_s.chomp) + dest_dir = get_dest_dir(field_name) + dest_file_path = "#{dest_dir}/#{filename}" + if File.exists?(dest_file_path) + uploaded_file_path_hash[field_name] = dest_file_path + else + upload_fields << field_name + end + end + end + return upload_fields, uploaded_file_path_hash + end + + def process_user_files_upload + process_upload_fields() + #get field that can take upload file + field_config = get_user_uploadfiles_fields_from_config() + + #get upload_fields that need upload ,such as ss.linux.config, ss.git.configxx + #get uploaded file info, we can add it in initrds + upload_fields, uploaded_file_path_hash = generate_upload_fields(field_config) + + # if upload_fields size > 0, need upload ,return + return upload_fields if !upload_fields.size.zero? + + #process if found file in server + uploaded_file_path_hash.each do |field, filepath| + # if field not match ss.*.config*, it is a simple job + if !(field =~ /ss\..*\.config.*/) + # construct initrd url for upload_file + # save initrd url in env upload_file_url, for append in PKGBUILD source=() + + initrd_http_prefix = "http://#{INITRD_HTTP_HOST}:#{INITRD_HTTP_PORT}" + upload_file_initrd = "#{initrd_http_prefix}#{JobHelper.service_path(filepath, true)}" + @hash["upload_file_url"] = JSON::Any.new(upload_file_initrd) + end + end + end + + private def process_upload_fields + return unless @hash.has_key?("upload_fields") + upload_fields = @hash["upload_fields"].not_nil!.as_a + # upload_fields: + # md5: xxx + # field_name: ss.xx.config* or pkgbuild config + # filename: basename of file + # content: file content + save_dirs = [] of String + upload_fields.each do |upload_item| + save_dirs << store_upload_file(upload_item.as_h) + end + reset_upload_field(upload_fields, save_dirs) + end + + private def store_upload_file(to_upload) + md5 = to_upload["md5"].to_s + field_name = to_upload["field_name"].to_s + file_name = to_upload["file_name"].to_s + dest_dir = get_dest_dir(field_name) + FileUtils.mkdir_p(dest_dir) unless File.exists?(dest_dir) + dest_file = "#{dest_dir}/#{file_name}" + #if file exist in server, check md5 + if File.exists?(dest_file) + return dest_file + end + #save file + content_base64 = to_upload["content"].to_s + dest_content = Base64.decode_string(content_base64) + File.touch(dest_file) + File.write(dest_file, dest_content) + # verify save + check_config_integrity(md5, dest_file) + return dest_file + end + + private def reset_upload_field(upload_fields, save_dirs) + new_upload_fields_data = [] of JSON::Any + # iter every upload_field, remove content, add save_dir + upload_fields.each_with_index do |item, index| + tmp = item.as_h + tmp["save_dir"] = JSON::Any.new(save_dirs[index]) + tmp.delete("content") if tmp.has_key?("content") + new_upload_fields_data << JSON::Any.new(tmp) + end + @hash["upload_fields"] = JSON::Any.new(new_upload_fields_data) + end end diff --git a/src/lib/user_uploadfiles_fields_config.yaml b/src/lib/user_uploadfiles_fields_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e93ef33f7fbd0597b3b911e84b0be911d488cd2f --- /dev/null +++ b/src/lib/user_uploadfiles_fields_config.yaml @@ -0,0 +1,4 @@ +#This file indicate what field in job can be treatd as a upload field +#For the below example, only when suite=pkgbuild and have config field, treat "config" as an upload field. +- suite: pkgbuild + field_name: config diff --git a/src/scheduler/auto_depend_submit_job.cr b/src/scheduler/auto_depend_submit_job.cr index 10a6e1b93be4f11fe7c82052a6e380a7e3d4fa12..f13f2b1acef33ef32bbd776f2293a279773f6123 100644 --- a/src/scheduler/auto_depend_submit_job.cr +++ b/src/scheduler/auto_depend_submit_job.cr @@ -8,6 +8,14 @@ class Sched job_content = JSON.parse(body) origin_job = init_job(job_content) + #if has upload_field, return it and notify client resubmit + upload_fields = origin_job.process_user_files_upload + if upload_fields + return [{ + "message" => "#{upload_fields}", + "errcode" => "RETRY_UPLOAD", + }] + end jobs = @env.cluster.handle_job(origin_job) jobs.each do |job| init_job_id(job) diff --git a/src/scheduler/constants.cr b/src/scheduler/constants.cr index d7b724e3d1671e52ff52604047514978a59b4375..bcbd04e9876bb07c41240b37073068135df760d9 100644 --- a/src/scheduler/constants.cr +++ b/src/scheduler/constants.cr @@ -39,6 +39,7 @@ LAB_REPO = "lab-#{LAB}" SRV_OS = "/srv/os" SRV_INITRD = "/srv/initrd" SRV_UPLOAD = "/srv/upload-files" +SRV_USER_FILE_UPLOAD = "/srv/cci/user-files" INITRD_HTTP_PREFIX = "http://#{INITRD_HTTP_HOST}:#{INITRD_HTTP_PORT}" OS_HTTP_PREFIX = "http://#{OS_HTTP_HOST}:#{OS_HTTP_PORT}" diff --git a/src/scheduler/plugins/pkgbuild.cr b/src/scheduler/plugins/pkgbuild.cr index c20d48e6ca49e3549693800348cd8b6c378037c5..5e7abb3f82940cd5e101c1ef300c4bdc5d9a345c 100644 --- a/src/scheduler/plugins/pkgbuild.cr +++ b/src/scheduler/plugins/pkgbuild.cr @@ -152,6 +152,22 @@ class PkgBuild < PluginsCommon # add user specify build params params.each do |k, v| + # if params key match config*, try link file to pkgbuild config dir + if k =~ /config.*/ + field_name = k + filename = File.basename(v.as_s) + #get origin uploaded_file + ss_upload_filepath = "#{SRV_USER_FILE_UPLOAD}/#{job["suite"]}/ss.#{pkg_name}.#{field_name}/#{filename}" + if File.exists?(ss_upload_filepath) + _pkgbuild_repo = content["pkgbuild_repo"].as_s + _pkg_name = _pkgbuild_repo.chomp.split('/', remove_empty: true)[-1] + dest_dir = "#{SRV_USER_FILE_UPLOAD}/pkgbuild/#{pkg_name}/#{field_name}" + pkg_dest_file = "#{dest_dir}/#{filename}" + FileUtils.mkdir_p(dest_dir) unless File.exists?(dest_dir) + #link file + File.symlink(ss_upload_filepath, pkg_dest_file) unless File.exists?(pkg_dest_file) + end + end content[k] = v end @@ -203,7 +219,7 @@ class PkgBuild < PluginsCommon end def load_default_pkgbuild_yaml - content = YAML.parse(File.open("#{ENV["LKP_SRC"]}/jobs/build-pkg.yaml")) + content = YAML.parse(File.open("#{ENV["LKP_SRC"]}/jobs/pkgbuild.yaml")) content = Hash(String, JSON::Any).from_json(content.to_json) return content