Docker&Kubernetes

Harbor迁移升级(从v1到v2)

2020-11-25  本文已影响0人  万州客

势在必行的计划,前期越周全越好。

一,升级原因

1,安全漏洞

Harbor 官方仓库公布了 5 个漏洞,其中包括 2 个官方定级为严重的漏洞(CVE-2019-19025、CVE-2019-19023),2个高危级别漏洞(CVE-2019-19029、CVE-2019-19026),1个中等级别漏洞(CVE-2019-3990)。

2,功能升级

Harbor 的 OCI Artifact 功能,可以用来存储、分发和管理机器学习的模型文件。

二,升级流程

harbor-migrate.png

三,升级步骤

先按v1版的harbor配置,建一个v2版的harbor(挂载大存储,ldap用户认证等)。

1,导出v1版本的项目和镜像列表

运行harbor_v1_images_export.py脚本,从V1版本harbor中,提取所有的项目列表和镜像文件列表。
为支持多次运行此脚本,会导入之前运行结果,以缩短再次运行的时间。
其中,项目列表为pro.csv文件,镜像列表为repo_v1.csv文件

"""
用于将老版本的harbor中的Project和repo镜像提取出来,保存到文件中。
admin
2020-11-24
"""
import requests

# 常量定义
harbor_domain_v1 = 'harbor.demo.cn'
username = 'admin'
password = 'xxxx'
pro_file = 'pro.csv'
repo_file = 'repo_v1.csv'


# V1版本,请求API使用session。
class RequestClient:
    def __init__(self, login_url, username, password):
        self.login_url = login_url
        self.username = username
        self.password = password
        self.session = requests.Session()
        self.login()

    def login(self):
        self.session.post(self.login_url,
                          params={'principal': self.username,
                                  'password': self.password})
        print('login', self.session)


# 将Harbor常用操作包装成一个class
class HarborReposV1:
    def __init__(self, harbor_domain, username, password, schema='http'):
        self.schema = schema
        self.harbor_domain = harbor_domain
        self.harbor_url = self.schema + '://' + harbor_domain
        self.harbor_login_url = self.harbor_url + '/login'
        self.harbor_api_url = self.harbor_url + '/api'
        self.harbor_pro_url = self.harbor_api_url + '/projects'
        self.harbor_repos_url = self.harbor_api_url + '/repositories'

        self.username = username
        self.password = password
        # self.client和self.pros_obj在初始化时就生成好,使用起来更流畅
        self.client = RequestClient(self.harbor_login_url,
                                    self.username,
                                    self.password)
        self.pros_obj = self.__fetch_pros_obj()

    def __fetch_pros_obj(self):
        return self.client.session.get(self.harbor_pro_url).json()

    # 获取所有的project id
    def fetch_pros_id(self):
        pros_id = list()
        for i in self.pros_obj:
            pros_id.append(i['project_id'])
        return pros_id

    # 根据project id获取project名称及public属性
    def fetch_pro_name(self, pro_id):
        for i in self.pros_obj:
            if i['project_id'] == pro_id:
                pro_name = i['name']
                pro_public = i['metadata']['public']
        return pro_name, pro_public

    # 根据project id获取此project下所有镜像名称
    def fetch_repos_name(self, pro_id):
        repos_name = list()
        repos_res = self.client.session.get(self.harbor_repos_url,
                                            params={'project_id': pro_id})
        for repo in repos_res.json():
            repos_name.append(repo['name'])
        return repos_name

    # 根据镜像名称,获取此镜像的所有tag
    def fetch_repos(self, repo_name):
        repos = list()
        harbor_tag_url = self.harbor_repos_url + '/' + repo_name + '/tags'
        repos_res = self.client.session.get(harbor_tag_url)
        for tag in repos_res.json():
            full_repo_name = '{}/{}:{}'.format(self.harbor_domain, repo_name, tag['name'])
            repos.append(full_repo_name)
        return repos


# 将文件内的所有行读取成列表
def read_file_list(file_name):
    with open(file_name, 'r') as f_r:
        return [line.strip() for line in f_r]


# 将指定内容追加到指定文件
def insert_file(file_name, content):
    # 对于python3, 第三个参数,指定编码方式必须要加encoding=“utf-8”
    with open(file_name, 'a', encoding='utf-8') as f_w:
        print(content)
        f_w.write('{}\r\n'.format(content))
        print("保存{}成功".format(content))


if __name__ == '__main__':
    # 初始化harbor连接数据
    res_v1 = HarborReposV1(harbor_domain_v1, username, password)
    # 如果文件中已存在项目和镜像,则先提取到列表里,避免重复插入,这样脚本就可以多次运行迁移
    try:
        pro_list = read_file_list(pro_file)
        repo_list = read_file_list(repo_file)
    except FileNotFoundError:  # 文件不能找到的异常处理
        pro_list = []
        repo_list = []
        print("首次运行,还没有文件,继续处理。。.")
    # 获取所有project id
    for pro_id in res_v1.fetch_pros_id():
        pro_name, is_public = res_v1.fetch_pro_name(pro_id)
        line_str = '{},{}'.format(pro_name, is_public)
        # 只将新的项目加入文件
        if line_str not in pro_list:
            insert_file(pro_file, line_str)
        else:
            print('project{}已存在于文件{}中'.format(line_str, pro_file))
        # 获取到所有镜像名称
        repos_name = res_v1.fetch_repos_name(pro_id=pro_id)
        for repo_name in repos_name:
            # 获取到镜像的所有tag
            repos = res_v1.fetch_repos(repo_name=repo_name)
            for full_repo_name in repos:
                # 只将新的镜像tag加入文件
                if full_repo_name not in repo_list:
                    insert_file(repo_file, full_repo_name)
                else:
                    print('镜像tag{}已存在于文件{}中'.format(full_repo_name, repo_file))

2,在v2版中导入项目列表

运行harbor_v2_projects_create.py脚本,将pro.csv文件中的项目列表导入新版harbor中,其中,保留了每个项目的public属性(true or false)。

"""
将从将仓库里获取到的所有project,在新仓库中重建好,带public属性的
admin
2020-11-24
"""
import requests
from requests.auth import HTTPBasicAuth
import json

pro_file = 'pro.csv'
harbor_domain_v2 = 'harbor-test.demo.cn:8086'
v2_username = 'admin'
v2_password = 'xxxxx'


# Harbor V2版本的class
class HarborReposV2:
    def __init__(self, harbor_domain, username, password, schema='http'):
        self.schema = schema
        self.harbor_domain = harbor_domain
        self.harbor_url = self.schema + '://' + harbor_domain
        # 新版harbor 2版本的api地址
        self.harbor_api_url = self.harbor_url + '/api/v2.0'
        self.harbor_pro_url = self.harbor_api_url + '/projects'

        self.username = username
        self.password = password
        # 使用HTTPBasicAuth认证,这也是避免那些CSRF Token Invalid的最佳办法
        # 是从harbor API里看认证方式获得的启发。
        self.auth = HTTPBasicAuth(self.username, self.password)

    # 好像只要三个要素,就可以新建一个project了。细节待完善。
    def create_pros(self, pro_name, is_public):
        pro_obj = dict()
        pro_obj['project_name'] = pro_name
        pro_obj["metadata"] = dict()
        pro_obj["metadata"]["public"] = is_public
        # pro_obj["metadata"]["enable_content_trust"] = i["enable_content_trust"]
        # pro_obj["metadata"]["prevent_vul"] = i["prevent_vulnerable_images_from_running"]
        # pro_obj["metadata"]["severity"] = i["prevent_vulnerable_images_from_running_severity"]
        # pro_obj["metadata"]["auto_scan"] = i["automatically_scan_images_on_push"]
        headers = {"content-type": "application/json"}
        res = requests.post(self.harbor_pro_url,
                            auth=self.auth,
                            headers=headers,
                            data=json.dumps(pro_obj))
        if res.status_code == 409:
            print("\033[32m 项目 %s 已经存在!\033[0m" % pro_name)
            return True
        elif res.status_code == 201:
            # print(res.status_code)
            print("\033[33m 创建项目%s成功!\033[0m" % pro_name)
            return True
        else:
            print("\033[35m 创建项目%s失败!\033[0m" % pro_name)
            return False


# 从旧仓库导出来的projects列表,读取出来
def read_file_list(file_name):
    with open(file_name, 'r') as f_r:
        return [line.strip() for line in f_r]


if __name__ == '__main__':
    # 初始化
    res_v2 = HarborReposV2(harbor_domain_v2, v2_username, v2_password)

    pro_list = read_file_list(pro_file)
    for item in pro_list:
        pro_name, is_public = item.split(',')
        res_v2.create_pros(pro_name, is_public)

3,在v2版中导入镜像列表

运行harbor_v2_images_import.py脚本,将repo_v1.csv文件中的镜像列表导入新版harbor中,同时,生成repo_v2.csv作为校验文件,以支持多次运行此脚本。
为了让中间的迁移机器不至于容量爆掉,在每迁移完一个镜像的所有tag之后,会删除此镜像的所有文件(每个镜像的所有tag,会共用基础层,如果导入一个镜像就删除一个镜像,效率会很慢,且重复传输严重,想你一个有300个tag的镜像)。

"""
用于保存到文件中镜像迁移到新的harbor镜像仓库当中。
admin
2020-11-24
"""
import subprocess

# 定义常量
repo_file = 'repo_v1.csv'
new_repo_file = 'repo_v2.csv'

harbor_domain_v1 = 'harbor.demo.cn'
v1_username = 'admin'
v1_password = 'xxxx'

harbor_domain_v2 = 'harbor-test.demo.cn:8086'
v2_username = 'admin'
v2_password = 'xxxx'

repo_list = list()
repo_dict = dict()
new_repo_list = list()


def read_file_list(file_name):
    with open(file_name, 'r') as f_r:
        return [line.strip() for line in f_r]


def insert_file(file_name, content):
    # 对于python3, 第三个参数,指定编码方式必须要加encoding=“utf-8”
    with open(file_name, 'a', encoding='utf-8') as f_w:
        print(content)
        f_w.write('{}\r\n'.format(content))
        print("保存{}成功".format(content))


# 从旧版的harbor中pull镜像,tag更名之后,push到新仓库,记得先登陆
def migrate_repos(v1_repo_name, v2_repo_name):
    cmd_list = []
    old_repo_login = "docker login {} -u {} -p {}".format(harbor_domain_v1, v1_username, v1_password)
    pull_old_repo = "docker pull " + v1_repo_name
    tag_repo = "docker tag " + v1_repo_name + " " + v2_repo_name
    new_repo_login = "docker login {} -u {} -p {}".format(harbor_domain_v2, v2_username, v2_password)
    push_new_repo = "docker push " + v2_repo_name
    cmd_list.append(old_repo_login)
    cmd_list.append(pull_old_repo)
    cmd_list.append(tag_repo)
    cmd_list.append(new_repo_login)
    cmd_list.append(push_new_repo)

    ret_sum = 0
    for cmd in cmd_list:
        print("\033[34m Current command: %s\033[0m" % cmd)
        ret = subprocess.call(cmd, shell=True)
        ret_sum += ret
    if ret_sum == 0:
        print("\033[32m migrate %s success!\033[0m" % v2_repo_name)
        insert_file(new_repo_file, v2_repo_name)
        return True

    else:
        print("\033[33m migrate %s faild!\033[0m" % v2_repo_name)
        return False


# 当一个镜像的所有tag迁移完成之后,清除此镜像所有tag,挪出空间,不然会爆掉
def delete_local_repos(repo, tags):
    for tag in tags:
        repo_tag = '{}:{}'.format(repo, tag)
        cmd = 'docker rmi {}'.format(repo_tag)
        if subprocess.call(cmd, shell=True) == 0:
            print("\033[32m 删除 {} 成功!\033[0m".format(repo_tag))
        else:
            print("\033[32m 删除 {} 失败,继续执行!\033[0m".format(repo_tag))


if __name__ == '__main__':
    try:
        new_repo_list = read_file_list(new_repo_file)
    except FileNotFoundError:
        print('首次导入。还没有文件。')
    # 这里的骚操作,是为了能让同一个镜像的不同tag,作同一批次的pull和push,
    # 操作完之后,才作docker image rmi的操作,肯定会显著缩短时间,
    # 因为同一个镜像的不同tag,很多层是相同的
    # 字典的键为镜像名,值为tag列表
    for item in read_file_list(repo_file):
        repo, tag = item.split(':')
        if repo not in repo_dict:
            repo_dict[repo] = [tag]
        else:
            repo_dict[repo].append(tag)
    # 遍历这个字典,作迁移
    for item in repo_dict.items():
        repo, tags = item
        # 新的仓库的镜像地址,需要整合新仓库的地址及旧仓库的项目repo名称
        repo_replace = repo.split('/')
        repo_replace[0] = harbor_domain_v2
        v2_repo = '/'.join(repo_replace)
        # demo小剂量测试
        if 'nginx-ingress-controller' in repo:
            for tag in tags:
                v1_repo_name = '{}:{}'.format(repo, tag)
                v2_repo_name = '{}:{}'.format(v2_repo, tag)
                # print(v1_repo_name)
                # print(v2_repo_name)
                # 已导入过的,忽略,减少时间
                if v2_repo_name in new_repo_list:
                    print('{}已导入新harbor仓库'.format(v2_repo_name))
                    continue
                # 真正的导出导入操作
                if not migrate_repos(v1_repo_name, v2_repo_name):
                    print('导入失败')
                    break
                else:
                    print('导入{}成功'.format(v2_repo_name))
            # 这里使用for...else...配合continue和break,可以直接跳出两个for循环外面
            else:
                print('{}导入完成,清除此镜像的所有tag'.format(repo))
                # 一个repo导入完成,清除新旧仓库的所有tag。
                delete_local_repos(repo, tags)
                delete_local_repos(v2_repo, tags)
                continue
            break

4,DNS切换

DNS切换,将指到v1版harbor的域名,指向v1版的harbor。

5,更新V2版配置

新版harbor更改配置,提供与域名一致的服务。
Harbor.yml


截屏2020-11-25下午1.29.07.png

重启harbor,使配置生效

docker-comppose down
./prepare
docker-compose up -d

(同时,v1版harbor更改为另外的域名或ip,不急马下线,待v2版稳定后下线,有个别镜像,还可以手工导入)

6,测试验证

在k8s环境,或是docker环境下,测试是否已平滑升级完成。

四,此种升级方案的优势和注意要点

在标准推荐的harbor升级方案中,从1.5到2.1,会涉及数据库的转换(从mysql转postgresql)。而我公司安装的harbor,是用的docker-compose方案,全docker部署,无形中增加了升级难度。
在我们设计的这个升级方案中,如果在DNS切换后,测试失败,是可以作回滚的,只要DNS切回即可。
另外,它也支持断点持续升级。也就是在空间和时间许可的情况下,分多个批次,来将V1版的镜像迁移到V2版中。而在DNS切换的这个维护时间窗口内,只需要迁移极少的镜像,花极少的时间来作最后的升级。而无须在短短一天之内,迁移上T的数据。

参考URL:
http://blog.nsfocus.net/cve-2019-19025-cve-2019-19023-cve-2019-19029-cve-2019-19026-cve-2019-3990/
https://www.cnblogs.com/breezey/p/10615242.html

上一篇 下一篇

猜你喜欢

热点阅读