Harbor迁移升级(从v1到v2)

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

一,升级原因

1,安全漏洞

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

  • CVE-2019-19025:缺少 CSRF 保护漏洞,Harbor Web界面未实现针对跨站点请求伪造(CSRF)的保护机制。通过把经过身份验证的用户吸引到事先准备好的第三方网站,可导致第三方代表经过身份验证的用户或管理员在平台上执行任意操作。
  • CVE-2019-19023:特权提升漏洞,该漏洞使普通用户可以通过API调用来修改特定用户的电子邮件地址,从而获得管理员帐户特权。漏洞源于Harbor API没有对修改电子邮件地址的API请求进行适当的权限限制。
  • CVE-2019-19029:通过用户组进行SQL注入,具有项目管理功能的用户可以利用SQL注入来从底层数据库读取机密信息或进行权限提升。
  • CVE-2019-19026:通过项目quotas进行SQL注入,Harbor API的quotas部分存在一个SQL注入漏洞。经过身份验证的管理员可以通过GET参数发送特制的SQL有效负载,从而从数据库中提取敏感信息。
  • CVE-2019-3990:用户枚举漏洞,该漏洞存在于 “/users” api 中,这个功能应该仅限于管理员使用,可是该限制可被绕过,非管理员用户(例如通过自我注册创建的用户)可以通过向 /api/users/search发送 GET请求来列出所有用户名和用户ID、确认与用户名关联的电子邮件地址等。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容