From c1fe6eceafb4464d602255a1773f0a47ae80efbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=8B=E6=B1=9F=E5=A4=A9=E6=9E=A2?= Date: Wed, 30 Jun 2021 15:13:45 +0800 Subject: [PATCH] update dubhe server --- dubhe-server/HELP.md | 9 + dubhe-server/admin/pom.xml | 154 + .../org/dubhe/admin/AdminApplication.java | 34 + .../dubhe/admin/client/AuthServiceClient.java | 54 + .../client/fallback/AuthServiceFallback.java | 55 + .../org/dubhe/admin/dao/AuthCodeMapper.java | 105 + .../dubhe/admin/dao/DataSequenceMapper.java | 59 + .../org/dubhe/admin/dao/DictDetailMapper.java | 59 + .../java/org/dubhe/admin/dao/DictMapper.java | 81 + .../java/org/dubhe/admin/dao/LogMapper.java | 45 + .../java/org/dubhe/admin/dao/MenuMapper.java | 83 + .../org/dubhe/admin/dao/PermissionMapper.java | 111 + .../dubhe/admin/dao/ResourceSpecsMapper.java | 27 + .../java/org/dubhe/admin/dao/RoleMapper.java | 163 + .../java/org/dubhe/admin/dao/TeamMapper.java | 58 + .../org/dubhe/admin/dao/UserAvatarMapper.java | 26 + .../org/dubhe/admin/dao/UserGroupMapper.java | 86 + .../java/org/dubhe/admin/dao/UserMapper.java | 147 + .../org/dubhe/admin/dao/UserRoleMapper.java | 46 + .../admin/dao/provider/MenuProvider.java | 45 + .../admin/dao/provider/RoleProvider.java | 43 + .../admin/dao/provider/TeamProvider.java | 32 + .../admin/dao/provider/UserProvider.java | 45 + .../admin/domain/dto/AuthCodeCreateDTO.java | 47 + .../admin/domain/dto/AuthCodeDeleteDTO.java | 36 + .../admin/domain/dto/AuthCodeQueryDTO.java | 37 + .../admin/domain/dto/AuthCodeUpdateDTO.java | 46 + .../domain/dto/AuthPermissionUpdDTO.java | 39 + .../dubhe/admin/domain/dto/AuthUpdateDTO.java | 71 + .../dubhe/admin/domain/dto/AuthUserDTO.java | 48 + .../dubhe/admin/domain/dto/DictCreateDTO.java | 51 + .../org/dubhe/admin/domain/dto/DictDTO.java | 42 + .../dubhe/admin/domain/dto/DictDeleteDTO.java | 36 + .../admin/domain/dto/DictDetailCreateDTO.java | 53 + .../dubhe/admin/domain/dto/DictDetailDTO.java | 53 + .../admin/domain/dto/DictDetailDeleteDTO.java | 38 + .../admin/domain/dto/DictDetailQueryDTO.java | 38 + .../admin/domain/dto/DictDetailUpdateDTO.java | 60 + .../dubhe/admin/domain/dto/DictQueryDTO.java | 35 + .../dubhe/admin/domain/dto/DictSmallDTO.java | 32 + .../admin/domain/dto/DictSmallQueryDTO.java | 33 + .../dubhe/admin/domain/dto/DictUpdateDTO.java | 58 + .../org/dubhe/admin/domain/dto/EmailDTO.java | 48 + .../dubhe/admin/domain/dto/ExtConfigDTO.java | 37 + .../org/dubhe/admin/domain/dto/LogDTO.java | 50 + .../dubhe/admin/domain/dto/LogQueryDTO.java | 41 + .../dubhe/admin/domain/dto/LogSmallDTO.java | 43 + .../dubhe/admin/domain/dto/MenuCreateDTO.java | 94 + .../org/dubhe/admin/domain/dto/MenuDTO.java | 82 + .../dubhe/admin/domain/dto/MenuDeleteDTO.java | 38 + .../dubhe/admin/domain/dto/MenuQueryDTO.java | 43 + .../dubhe/admin/domain/dto/MenuUpdateDTO.java | 117 + .../org/dubhe/admin/domain/dto/NodeDTO.java | 72 + .../admin/domain/dto/PermissionCreateDTO.java | 43 + .../dubhe/admin/domain/dto/PermissionDTO.java | 43 + .../admin/domain/dto/PermissionDeleteDTO.java | 34 + .../admin/domain/dto/PermissionQueryDTO.java | 33 + .../admin/domain/dto/PermissionUpdateDTO.java | 47 + .../org/dubhe/admin/domain/dto/PodDTO.java | 52 + .../domain/dto/ResourceSpecsCreateDTO.java | 74 + .../domain/dto/ResourceSpecsDeleteDTO.java | 40 + .../domain/dto/ResourceSpecsQueryDTO.java | 51 + .../domain/dto/ResourceSpecsUpdateDTO.java | 77 + .../admin/domain/dto/RoleAuthUpdateDTO.java | 39 + .../dubhe/admin/domain/dto/RoleCreateDTO.java | 55 + .../org/dubhe/admin/domain/dto/RoleDTO.java | 48 + .../dubhe/admin/domain/dto/RoleDeleteDTO.java | 38 + .../dubhe/admin/domain/dto/RoleQueryDTO.java | 44 + .../dubhe/admin/domain/dto/RoleSmallDTO.java | 35 + .../dubhe/admin/domain/dto/RoleUpdateDTO.java | 75 + .../dubhe/admin/domain/dto/TeamCreateDTO.java | 52 + .../dubhe/admin/domain/dto/TeamQueryDTO.java | 50 + .../dubhe/admin/domain/dto/TeamUpdateDTO.java | 55 + .../domain/dto/TeamUserRoleSmallDTO.java | 33 + .../admin/domain/dto/UserAvatarUpdateDTO.java | 34 + .../admin/domain/dto/UserCenterUpdateDTO.java | 55 + .../dubhe/admin/domain/dto/UserCreateDTO.java | 84 + .../dubhe/admin/domain/dto/UserDeleteDTO.java | 40 + .../admin/domain/dto/UserEmailUpdateDTO.java | 62 + .../dubhe/admin/domain/dto/UserGroupDTO.java | 50 + .../admin/domain/dto/UserGroupDeleteDTO.java | 36 + .../admin/domain/dto/UserGroupQueryDTO.java | 41 + .../admin/domain/dto/UserGroupUpdDTO.java | 43 + .../admin/domain/dto/UserPassUpdateDTO.java | 36 + .../dubhe/admin/domain/dto/UserQueryDTO.java | 53 + .../admin/domain/dto/UserRegisterDTO.java | 72 + .../admin/domain/dto/UserRegisterMailDTO.java | 51 + .../domain/dto/UserResetPasswordDTO.java | 57 + .../admin/domain/dto/UserRoleUpdateDTO.java | 43 + .../admin/domain/dto/UserStateUpdateDTO.java | 40 + .../dubhe/admin/domain/dto/UserUpdateDTO.java | 86 + .../org/dubhe/admin/domain/entity/Auth.java | 43 + .../admin/domain/entity/AuthPermission.java | 45 + .../admin/domain/entity/DataSequence.java | 45 + .../org/dubhe/admin/domain/entity/Dict.java | 75 + .../dubhe/admin/domain/entity/DictDetail.java | 77 + .../org/dubhe/admin/domain/entity/Group.java | 46 + .../org/dubhe/admin/domain/entity/Log.java | 106 + .../org/dubhe/admin/domain/entity/Menu.java | 109 + .../dubhe/admin/domain/entity/Permission.java | 48 + .../admin/domain/entity/ResourceSpecs.java | 87 + .../org/dubhe/admin/domain/entity/Role.java | 69 + .../dubhe/admin/domain/entity/RoleAuth.java | 40 + .../dubhe/admin/domain/entity/RoleMenu.java | 49 + .../org/dubhe/admin/domain/entity/Team.java | 68 + .../admin/domain/entity/TeamUserRole.java | 58 + .../org/dubhe/admin/domain/entity/User.java | 94 + .../dubhe/admin/domain/entity/UserAvatar.java | 58 + .../dubhe/admin/domain/entity/UserRole.java | 47 + .../org/dubhe/admin/domain/vo/AuthVO.java | 59 + .../org/dubhe/admin/domain/vo/EmailVo.java | 41 + .../org/dubhe/admin/domain/vo/MenuMetaVo.java | 39 + .../org/dubhe/admin/domain/vo/MenuVo.java | 44 + .../dubhe/admin/domain/vo/PermissionVO.java | 47 + .../admin/domain/vo/ResourceSpecsQueryVO.java | 71 + .../dubhe/admin/domain/vo/UserGroupVO.java | 54 + .../org/dubhe/admin/domain/vo/UserVO.java | 58 + .../org/dubhe/admin/enums/MenuTypeEnum.java | 101 + .../org/dubhe/admin/enums/SystemNodeEnum.java | 80 + .../dubhe/admin/enums/UserMailCodeEnum.java | 96 + .../java/org/dubhe/admin/event/BaseEvent.java | 49 + .../org/dubhe/admin/event/EmailEvent.java | 39 + .../dubhe/admin/event/EmailEventListener.java | 71 + .../admin/event/EmailEventPublisher.java | 53 + .../dubhe/admin/rest/AuthCodeController.java | 88 + .../org/dubhe/admin/rest/DictController.java | 108 + .../admin/rest/DictDetailController.java | 96 + .../dubhe/admin/rest/ForwardController.java | 126 + .../org/dubhe/admin/rest/LogController.java | 112 + .../org/dubhe/admin/rest/LoginController.java | 175 + .../org/dubhe/admin/rest/MailController.java | 50 + .../org/dubhe/admin/rest/MenuController.java | 110 + .../admin/rest/PermissionController.java | 89 + .../admin/rest/RecycleTaskController.java | 79 + .../admin/rest/ResourceSpecsController.java | 82 + .../org/dubhe/admin/rest/RoleController.java | 142 + .../org/dubhe/admin/rest/TeamController.java | 96 + .../admin/rest/UserCenterController.java | 140 + .../org/dubhe/admin/rest/UserController.java | 118 + .../dubhe/admin/rest/UserGroupController.java | 133 + .../dubhe/admin/service/AuthCodeService.java | 80 + .../admin/service/DataSequenceService.java | 60 + .../admin/service/DictDetailService.java | 89 + .../org/dubhe/admin/service/DictService.java | 101 + .../org/dubhe/admin/service/LogService.java | 95 + .../org/dubhe/admin/service/MailService.java | 74 + .../org/dubhe/admin/service/MenuService.java | 136 + .../admin/service/PermissionService.java | 83 + .../admin/service/RecycleTaskService.java | 84 + .../admin/service/ResourceSpecsService.java | 65 + .../org/dubhe/admin/service/RoleService.java | 141 + .../org/dubhe/admin/service/TeamService.java | 102 + .../dubhe/admin/service/UserGroupService.java | 112 + .../org/dubhe/admin/service/UserService.java | 224 ++ .../admin/service/convert/DictConvert.java | 33 + .../service/convert/DictDetailConvert.java | 31 + .../service/convert/DictSmallConvert.java | 32 + .../admin/service/convert/LogConvert.java | 32 + .../admin/service/convert/MenuConvert.java | 32 + .../service/convert/PermissionConvert.java | 31 + .../admin/service/convert/RoleConvert.java | 30 + .../service/convert/RoleSmallConvert.java | 31 + .../admin/service/convert/TeamConvert.java | 33 + .../service/convert/TeamSmallConvert.java | 32 + .../admin/service/convert/UserConvert.java | 31 + .../service/impl/AuthCodeServiceImpl.java | 236 ++ .../service/impl/DataSequenceServiceImpl.java | 102 + .../service/impl/DictDetailServiceImpl.java | 168 + .../admin/service/impl/DictServiceImpl.java | 208 ++ .../admin/service/impl/LogServiceImpl.java | 148 + .../admin/service/impl/MailServiceImpl.java | 150 + .../admin/service/impl/MenuServiceImpl.java | 437 +++ .../service/impl/PermissionServiceImpl.java | 216 ++ .../service/impl/RecycleTaskServiceImpl.java | 475 +++ .../impl/ResourceSpecsServiceImpl.java | 209 ++ .../admin/service/impl/RoleServiceImpl.java | 276 ++ .../admin/service/impl/TeamServiceImpl.java | 169 + .../service/impl/UserGroupServiceImpl.java | 292 ++ .../admin/service/impl/UserServiceImpl.java | 907 +++++ .../task/RecycleInvalidResourcesTask.java | 56 + .../admin/task/RecycleResourcesTask.java | 59 + .../admin/src/main/resources/banner.txt | 14 + .../admin/src/main/resources/bootstrap.yml | 37 + .../src/main/resources/mapper/AuthMapper.xml | 18 + .../src/main/resources/mapper/MenuMapper.xml | 11 + .../resources/mapper/PermissionMapper.xml | 15 + .../src/main/resources/mapper/UserMapper.xml | 16 + .../main/resources/mapper/UserRoleMapper.xml | 19 + .../dubhe/admin/AdminApplicationTests.java | 70 + .../org/dubhe/admin/DictControllerTest.java | 43 + dubhe-server/auth/pom.xml | 92 + .../java/org/dubhe/auth/AuthApplication.java | 33 + .../config/AuthorizationServerConfig.java | 100 + .../org/dubhe/auth/config/SecurityConfig.java | 83 + .../exception/AuthenticationProviderImpl.java | 76 + .../exception/CustomerOauthException.java | 37 + .../CustomerOauthExceptionSerializer.java | 49 + ...erOauthWebResponseExceptionTranslator.java | 168 + .../org/dubhe/auth/rest/AuthController.java | 118 + .../auth/src/main/resources/banner.txt | 13 + .../auth/src/main/resources/bootstrap.yml | 29 + .../org/dubhe/auth/AuthApplicationTests.java | 13 + dubhe-server/common-biz/base/pom.xml | 75 + .../dubhe/biz/base/annotation/ApiVersion.java | 32 + .../biz/base/annotation/DataPermission.java | 41 + .../dubhe/biz/base/annotation/EnumValue.java | 88 + .../biz/base/annotation/FlagValidator.java | 66 + .../org/dubhe/biz/base/annotation/Log.java | 23 + .../base/constant/ApplicationNameConst.java | 84 + .../dubhe/biz/base/constant/AuthConst.java | 77 + .../base/constant/DataStateCodeConstant.java | 84 + .../base/constant/FileStateCodeConstant.java | 54 + .../biz/base/constant/HarborProperties.java | 37 + .../biz/base/constant/MagicNumConstant.java | 109 + .../base/constant/MetaHandlerConstant.java | 53 + .../biz/base/constant/NumberConstant.java | 48 + .../dubhe/biz/base/constant/Permissions.java | 205 ++ .../dubhe/biz/base/constant/ResponseCode.java | 34 + .../biz/base/constant/StringConstant.java | 115 + .../biz/base/constant/SymbolConstant.java | 55 + .../dubhe/biz/base/constant/UserConstant.java | 94 + .../dubhe/biz/base/context/DataContext.java | 62 + .../dubhe/biz/base/context/UserContext.java | 76 + .../java/org/dubhe/biz/base/dto/BaseDTO.java | 48 + .../org/dubhe/biz/base/dto/BaseImageDTO.java | 47 + .../biz/base/dto/CommonPermissionDataDTO.java | 48 + .../dto/DictDetailQueryByLabelNameDTO.java | 35 + .../base/dto/ModelOptAlgorithmCreateDTO.java | 48 + .../base/dto/NoteBookAlgorithmQueryDTO.java | 41 + .../base/dto/NoteBookAlgorithmUpdateDTO.java | 49 + .../dubhe/biz/base/dto/Oauth2TokenDTO.java | 49 + .../dubhe/biz/base/dto/PtDatasetSmallDTO.java | 32 + .../biz/base/dto/PtImageQueryUrlDTO.java | 40 + .../dto/PtModelBranchConditionQueryDTO.java | 39 + .../base/dto/PtModelBranchQueryByIdDTO.java | 38 + .../dto/PtModelInfoConditionQueryDTO.java | 54 + .../biz/base/dto/PtModelInfoQueryByIdDTO.java | 38 + .../biz/base/dto/PtModelStatusQueryDTO.java | 45 + .../dubhe/biz/base/dto/PtStorageSmallDTO.java | 33 + .../dto/PtTrainDataSourceStatusQueryDTO.java | 41 + .../biz/base/dto/QueryResourceSpecsDTO.java | 54 + .../dubhe/biz/base/dto/SysPermissionDTO.java | 38 + .../org/dubhe/biz/base/dto/SysRoleDTO.java | 48 + .../java/org/dubhe/biz/base/dto/TeamDTO.java | 53 + .../org/dubhe/biz/base/dto/TeamSmallDTO.java | 36 + .../TrainAlgorithmSelectAllBatchIdDTO.java | 36 + .../dto/TrainAlgorithmSelectAllByIdDTO.java | 35 + .../base/dto/TrainAlgorithmSelectByIdDTO.java | 35 + .../java/org/dubhe/biz/base/dto/UserDTO.java | 63 + .../org/dubhe/biz/base/dto/UserSmallDTO.java | 38 + .../biz/base/enums/AlgorithmSourceEnum.java | 46 + .../biz/base/enums/AlgorithmStatusEnum.java | 63 + .../biz/base/enums/BaseErrorCodeEnum.java | 71 + .../org/dubhe/biz/base/enums/BizEnum.java | 98 + .../dubhe/biz/base/enums/DatasetTypeEnum.java | 67 + .../dubhe/biz/base/enums/ImageSourceEnum.java | 50 + .../dubhe/biz/base/enums/ImageStateEnum.java | 53 + .../dubhe/biz/base/enums/ImageTypeEnum.java | 97 + .../biz/base/enums/MeasureStateEnum.java | 53 + .../biz/base/enums/ModelResourceEnum.java | 66 + .../biz/base/enums/OperationTypeEnum.java | 69 + .../biz/base/enums/ResourcesPoolTypeEnum.java | 61 + .../org/dubhe/biz/base/enums/SwitchEnum.java | 95 + .../dubhe/biz/base/enums/SystemNodeEnum.java | 80 + .../biz/base/exception/BusinessException.java | 72 + .../biz/base/exception/CaptchaException.java | 48 + .../base/exception/DataSequenceException.java | 43 + .../dubhe/biz/base/exception/ErrorCode.java | 39 + .../biz/base/exception/FeignException.java | 72 + .../base/exception/OAuthResponseError.java | 45 + .../biz/base/functional/StringFormat.java | 32 + .../biz/base/service/UserContextService.java | 37 + .../org/dubhe/biz/base/utils/AesUtil.java | 94 + .../org/dubhe/biz/base/utils/DateUtil.java | 128 + .../org/dubhe/biz/base/utils/HttpUtils.java | 66 + .../org/dubhe/biz/base/utils/MathUtils.java | 75 + .../org/dubhe/biz/base/utils/Md5Util.java | 53 + .../org/dubhe/biz/base/utils/NumberUtil.java | 46 + .../org/dubhe/biz/base/utils/PtModelUtil.java | 59 + .../org/dubhe/biz/base/utils/RandomUtil.java | 37 + .../dubhe/biz/base/utils/ReflectionUtils.java | 44 + .../org/dubhe/biz/base/utils/RegexUtil.java | 76 + .../org/dubhe/biz/base/utils/RsaEncrypt.java | 150 + .../biz/base/utils/SpringContextHolder.java | 94 + .../org/dubhe/biz/base/utils/StringUtils.java | 417 +++ .../biz/base/utils/TimeTransferUtil.java | 49 + .../java/org/dubhe/biz/base/vo/BaseVO.java | 41 + .../dubhe/biz/base/vo/DataResponseBody.java | 78 + .../java/org/dubhe/biz/base/vo/DatasetVO.java | 160 + .../org/dubhe/biz/base/vo/DictDetailVO.java | 67 + .../java/org/dubhe/biz/base/vo/DictVO.java | 43 + .../biz/base/vo/ModelOptAlgorithmQureyVO.java | 136 + .../org/dubhe/biz/base/vo/ProgressVO.java | 51 + .../biz/base/vo/PtModelBranchQueryVO.java | 130 + .../dubhe/biz/base/vo/PtModelInfoQueryVO.java | 127 + .../biz/base/vo/QueryResourceSpecsVO.java | 94 + .../biz/base/vo/TrainAlgorithmQureyVO.java | 136 + .../common-biz/data-permission/pom.xml | 39 + .../annotation/DataPermissionMethod.java | 48 + .../permission/annotation/RolePermission.java | 34 + .../permission/aspect/PermissionAspect.java | 122 + .../aspect/RolePermissionAspect.java | 57 + .../biz/permission/base/BaseService.java | 81 + .../permission/config/MetaHandlerConfig.java | 80 + .../permission/config/MybatisPlusConfig.java | 44 + .../interceptor/CustomerSqlInterceptor.java | 117 + .../interceptor/PaginationInterceptor.java | 463 +++ .../dubhe/biz/permission/util/SqlUtil.java | 114 + dubhe-server/common-biz/data-response/pom.xml | 48 + .../handler/GlobalExceptionHandler.java | 155 + .../factory/DataResponseFactory.java | 123 + dubhe-server/common-biz/db/pom.xml | 62 + .../org/dubhe/biz/db/annotation/Query.java | 68 + .../org/dubhe/biz/db/base/BaseConvert.java | 58 + .../org/dubhe/biz/db/base/PageQueryBase.java | 63 + .../biz/db/constant/MetaHandlerConstant.java | 53 + .../biz/db/constant/PermissionConstant.java | 41 + .../org/dubhe/biz/db/entity/BaseEntity.java | 76 + .../java/org/dubhe/biz/db/utils/PageUtil.java | 78 + .../org/dubhe/biz/db/utils/WrapperHelp.java | 162 + dubhe-server/common-biz/file/pom.xml | 82 + .../org/dubhe/biz/file/api/FileStoreApi.java | 237 ++ .../file/api/impl/HostFileStoreApiImpl.java | 666 ++++ .../file/api/impl/NfsFileStoreApiImpl.java | 152 + .../file/api/impl/ShellFileStoreApiImpl.java | 267 ++ .../org/dubhe/biz/file/config/NfsConfig.java | 41 + .../java/org/dubhe/biz/file/dto/FileDTO.java | 54 + .../org/dubhe/biz/file/dto/FilePageDTO.java | 51 + .../dubhe/biz/file/dto/MinioDownloadDTO.java | 60 + .../org/dubhe/biz/file/enums/BizPathEnum.java | 105 + .../dubhe/biz/file/enums/CopyTypeEnum.java | 54 + .../biz/file/exception/NfsBizException.java | 42 + .../dubhe/biz/file/utils/DubheFileUtil.java | 410 +++ .../java/org/dubhe/biz/file/utils/IOUtil.java | 47 + .../dubhe/biz/file/utils/LocalFileUtil.java | 434 +++ .../org/dubhe/biz/file/utils/MinioUtil.java | 283 ++ .../biz/file/utils/MinioWebTokenBody.java | 81 + .../org/dubhe/biz/file/utils/NfsFactory.java | 78 + .../org/dubhe/biz/file/utils/NfsPool.java | 116 + .../org/dubhe/biz/file/utils/NfsUtil.java | 776 +++++ dubhe-server/common-biz/log/pom.xml | 46 + .../org/dubhe/biz/log/aspect/LogAspect.java | 81 + .../org/dubhe/biz/log/entity/LogInfo.java | 59 + .../java/org/dubhe/biz/log/enums/LogEnum.java | 95 + .../dubhe/biz/log/filter/BaseLogFilter.java | 79 + .../biz/log/filter/ConsoleLogFilter.java | 50 + .../log/filter/GlobalRequestLogFilter.java | 34 + .../biz/log/handler/ScheduleTaskHandler.java | 45 + .../java/org/dubhe/biz/log/utils/LogUtil.java | 321 ++ .../log/src/main/resources/logback.xml | 262 ++ dubhe-server/common-biz/pom.xml | 38 + dubhe-server/common-biz/redis/pom.xml | 34 + .../dubhe/biz/redis/config/RedisConfig.java | 215 ++ .../org/dubhe/biz/redis/utils/RedisUtils.java | 865 +++++ dubhe-server/common-biz/state-machine/pom.xml | 48 + .../biz/statemachine/dto/StateChangeDTO.java | 49 + .../exception/StateMachineException.java | 49 + .../utils/StateMachineProxyUtil.java | 124 + dubhe-server/common-cloud/auth-config/pom.xml | 60 + .../config/ResourceServerConfig.java | 126 + ...uthenticationThreadLocalTaskDecorator.java | 51 + .../cloud/authconfig/dto/JwtUserDTO.java | 92 + .../handler/CustomerAccessDeniedHandler.java | 49 + .../CustomerTokenExceptionEntryPoint.java | 50 + .../factory/AccessTokenConverterFactory.java | 51 + .../factory/PasswordEncoderFactory.java | 44 + .../factory/TokenServicesFactory.java | 54 + .../authconfig/factory/TokenStoreFactory.java | 41 + .../filter/OAuth2ResponseErrorFilter.java | 71 + .../cloud/authconfig/service/AdminClient.java | 51 + .../service/AdminClientFallback.java | 47 + .../authconfig/service/AdminUserService.java | 39 + .../service/impl/GrantedAuthorityImpl.java | 43 + .../impl/OAuth2UserContextServiceImpl.java | 43 + .../service/impl/UserDetailsServiceImpl.java | 99 + .../cloud/authconfig/utils/JwtUtils.java | 63 + .../common-cloud/configuration/pom.xml | 30 + .../main/resources/bootstrap-cloud-data.yml | 7 + .../main/resources/bootstrap-cloud-dev.yml | 7 + .../main/resources/bootstrap-cloud-test.yml | 7 + .../src/main/resources/bootstrap-dev.yml | 7 + .../main/resources/bootstrap-istio-dev.yml | 7 + .../main/resources/bootstrap-istio-test.yml | 7 + .../src/main/resources/bootstrap-prod.yml | 7 + dubhe-server/common-cloud/pom.xml | 37 + .../common-cloud/registration/pom.xml | 29 + .../registration/RegistrationConfig.java | 29 + dubhe-server/common-cloud/remote-call/pom.xml | 71 + .../remotecall/config/FeignErrorDecoder.java | 54 + .../remotecall/config/HttpClientConfig.java | 125 + .../remotecall/config/RemoteCallConfig.java | 44 + .../remotecall/config/RestTemplateConfig.java | 61 + .../remotecall/config/RestTemplateHolder.java | 95 + .../interceptor/FeignInterceptor.java | 50 + .../RestTemplateTokenInterceptor.java | 97 + ...stAttributeHystrixConcurrencyStrategy.java | 141 + dubhe-server/common-cloud/swagger/pom.xml | 48 + .../cloud/swagger/config/SwaggerConfig.java | 134 + dubhe-server/common-cloud/unit-test/pom.xml | 59 + .../dubhe/cloud/unittest/base/BaseTest.java | 165 + .../cloud/unittest/config/UnitTestConfig.java | 38 + dubhe-server/common-k8s/.gitignore | 1 + dubhe-server/common-k8s/pom.xml | 92 + .../java/org/dubhe/harbor/api/HarborApi.java | 70 + .../dubhe/harbor/api/impl/HarborApiImpl.java | 312 ++ .../org/dubhe/harbor/config/HarborConfig.java | 35 + .../dubhe/harbor/domain/vo/ImagePageVO.java | 35 + .../org/dubhe/harbor/domain/vo/ImageVO.java | 37 + .../dubhe/harbor/utils/HttpClientUtils.java | 169 + .../abstracts/AbstractDeploymentCallback.java | 85 + .../k8s/abstracts/AbstractPodCallback.java | 83 + .../org/dubhe/k8s/annotation/K8sField.java | 39 + .../dubhe/k8s/annotation/K8sValidation.java | 35 + .../org/dubhe/k8s/api/DistributeTrainApi.java | 68 + .../org/dubhe/k8s/api/DubheDeploymentApi.java | 70 + .../org/dubhe/k8s/api/JupyterResourceApi.java | 73 + .../java/org/dubhe/k8s/api/LimitRangeApi.java | 56 + .../org/dubhe/k8s/api/LogMonitoringApi.java | 78 + .../java/org/dubhe/k8s/api/MetricsApi.java | 107 + .../org/dubhe/k8s/api/ModelOptJobApi.java | 71 + .../org/dubhe/k8s/api/ModelServingApi.java | 52 + .../java/org/dubhe/k8s/api/NamespaceApi.java | 145 + .../org/dubhe/k8s/api/NativeResourceApi.java | 41 + .../main/java/org/dubhe/k8s/api/NodeApi.java | 210 ++ .../k8s/api/PersistentVolumeClaimApi.java | 113 + .../main/java/org/dubhe/k8s/api/PodApi.java | 173 + .../dubhe/k8s/api/ResourceIisolationApi.java | 57 + .../org/dubhe/k8s/api/ResourceQuotaApi.java | 77 + .../java/org/dubhe/k8s/api/TrainJobApi.java | 64 + .../java/org/dubhe/k8s/api/VolumeApi.java | 29 + .../k8s/api/impl/DistributeTrainApiImpl.java | 516 +++ .../k8s/api/impl/DubheDeploymentApiImpl.java | 439 +++ .../k8s/api/impl/JupyterResourceApiImpl.java | 692 ++++ .../dubhe/k8s/api/impl/LimitRangeApiImpl.java | 128 + .../k8s/api/impl/LogMonitoringApiImpl.java | 369 +++ .../dubhe/k8s/api/impl/MetricsApiImpl.java | 498 +++ .../k8s/api/impl/ModelOptJobApiImpl.java | 432 +++ .../k8s/api/impl/ModelServingApiImpl.java | 337 ++ .../dubhe/k8s/api/impl/NamespaceApiImpl.java | 332 ++ .../k8s/api/impl/NativeResourceApiImpl.java | 72 + .../org/dubhe/k8s/api/impl/NodeApiImpl.java | 723 ++++ .../impl/PersistentVolumeClaimApiImpl.java | 369 +++ .../org/dubhe/k8s/api/impl/PodApiImpl.java | 446 +++ .../api/impl/ResourceIisolationApiImpl.java | 124 + .../k8s/api/impl/ResourceQuotaApiImpl.java | 236 ++ .../dubhe/k8s/api/impl/TrainJobApiImpl.java | 485 +++ .../org/dubhe/k8s/api/impl/VolumeApiImpl.java | 128 + .../dubhe/k8s/aspect/ValidationAspect.java | 166 + .../org/dubhe/k8s/cache/ResourceCache.java | 238 ++ .../k8s/config/K8sCallbackMvcConfig.java | 50 + .../java/org/dubhe/k8s/config/K8sConfig.java | 178 + .../org/dubhe/k8s/config/K8sNameConfig.java | 44 + .../dubhe/k8s/constant/K8sLabelConstants.java | 69 + .../dubhe/k8s/constant/K8sParamConstants.java | 97 + .../dubhe/k8s/constant/RedisConstants.java | 47 + .../org/dubhe/k8s/dao/K8sResourceMapper.java | 28 + .../java/org/dubhe/k8s/dao/K8sTaskMapper.java | 58 + .../org/dubhe/k8s/domain/PtBaseResult.java | 80 + .../dubhe/k8s/domain/bo/BuildFsVolumeBO.java | 39 + .../dubhe/k8s/domain/bo/BuildIngressBO.java | 121 + .../dubhe/k8s/domain/bo/BuildServiceBO.java | 58 + .../k8s/domain/bo/DistributeTrainBO.java | 142 + .../org/dubhe/k8s/domain/bo/K8sTaskBO.java | 40 + .../java/org/dubhe/k8s/domain/bo/LabelBO.java | 61 + .../dubhe/k8s/domain/bo/LogMonitoringBO.java | 74 + .../dubhe/k8s/domain/bo/ModelServingBO.java | 135 + .../k8s/domain/bo/PrometheusMetricBO.java | 120 + .../dubhe/k8s/domain/bo/PtDeploymentBO.java | 60 + .../java/org/dubhe/k8s/domain/bo/PtJobBO.java | 60 + .../dubhe/k8s/domain/bo/PtJupyterJobBO.java | 100 + .../k8s/domain/bo/PtJupyterResourceBO.java | 104 + .../dubhe/k8s/domain/bo/PtLimitRangeBO.java | 40 + .../bo/PtModelOptimizationDeploymentBO.java | 66 + .../domain/bo/PtModelOptimizationJobBO.java | 71 + .../org/dubhe/k8s/domain/bo/PtMountDirBO.java | 51 + .../domain/bo/PtPersistentVolumeClaimBO.java | 111 + .../k8s/domain/bo/PtResourceQuotaBO.java | 81 + .../dubhe/k8s/domain/bo/ResourceYamlBO.java | 41 + .../org/dubhe/k8s/domain/bo/TaskYamlBO.java | 43 + .../dubhe/k8s/domain/cr/DistributeTrain.java | 62 + .../domain/cr/DistributeTrainDoneable.java | 30 + .../k8s/domain/cr/DistributeTrainList.java | 26 + .../k8s/domain/cr/DistributeTrainSpec.java | 175 + .../BaseK8sDeploymentCallbackCreateDTO.java | 82 + .../dto/BaseK8sPodCallbackCreateDTO.java | 87 + .../k8s/domain/dto/NodeIsolationDTO.java | 40 + .../domain/dto/PodLogDownloadQueryDTO.java | 47 + .../dubhe/k8s/domain/dto/PodLogQueryDTO.java | 97 + .../org/dubhe/k8s/domain/dto/PodQueryDTO.java | 84 + .../dubhe/k8s/domain/entity/K8sResource.java | 65 + .../org/dubhe/k8s/domain/entity/K8sTask.java | 110 + .../k8s/domain/resource/BizContainer.java | 46 + .../resource/BizContainerStateTerminated.java | 39 + .../resource/BizContainerStateWaiting.java | 35 + .../domain/resource/BizContainerStatus.java | 41 + .../k8s/domain/resource/BizDeployment.java | 84 + .../resource/BizDeploymentCondition.java | 41 + .../domain/resource/BizDistributeTrain.java | 66 + .../resource/BizDistributeTrainContainer.java | 47 + .../resource/BizDistributeTrainResources.java | 35 + .../domain/resource/BizHTTPIngressPath.java | 35 + .../dubhe/k8s/domain/resource/BizIngress.java | 58 + .../k8s/domain/resource/BizIngressRule.java | 49 + .../k8s/domain/resource/BizIngressTLS.java | 35 + .../k8s/domain/resource/BizIntOrString.java | 37 + .../org/dubhe/k8s/domain/resource/BizJob.java | 54 + .../k8s/domain/resource/BizJobCondition.java | 45 + .../k8s/domain/resource/BizLimitRange.java | 52 + .../domain/resource/BizLimitRangeItem.java | 46 + .../k8s/domain/resource/BizNamespace.java | 53 + .../dubhe/k8s/domain/resource/BizNode.java | 111 + .../k8s/domain/resource/BizNodeAddress.java | 35 + .../k8s/domain/resource/BizNodeCondition.java | 65 + .../domain/resource/BizNodeSystemInfo.java | 51 + .../resource/BizPersistentVolumeClaim.java | 72 + .../BizPersistentVolumeClaimCondition.java | 44 + .../org/dubhe/k8s/domain/resource/BizPod.java | 128 + .../k8s/domain/resource/BizPodCondition.java | 54 + .../k8s/domain/resource/BizPodMetrics.java | 61 + .../k8s/domain/resource/BizQuantity.java | 61 + .../k8s/domain/resource/BizResourceQuota.java | 77 + .../BizScopedResourceSelectorRequirement.java | 49 + .../dubhe/k8s/domain/resource/BizSecret.java | 37 + .../dubhe/k8s/domain/resource/BizService.java | 37 + .../dubhe/k8s/domain/resource/BizTaint.java | 39 + .../dubhe/k8s/domain/resource/BizVolume.java | 37 + .../k8s/domain/resource/BizVolumeMount.java | 37 + .../org/dubhe/k8s/domain/vo/GpuUsageVO.java | 38 + .../dubhe/k8s/domain/vo/LogMonitoringVO.java | 43 + .../k8s/domain/vo/MetricsDataResultVO.java | 44 + .../domain/vo/MetricsDataResultValueVO.java | 42 + .../dubhe/k8s/domain/vo/ModelServingVO.java | 48 + .../dubhe/k8s/domain/vo/PodLogQueryVO.java | 50 + .../k8s/domain/vo/PodRangeMetricsVO.java | 54 + .../java/org/dubhe/k8s/domain/vo/PodVO.java | 41 + .../k8s/domain/vo/PtContainerMetricsVO.java | 98 + .../k8s/domain/vo/PtJupyterDeployVO.java | 129 + .../dubhe/k8s/domain/vo/PtJupyterJobVO.java | 50 + .../dubhe/k8s/domain/vo/PtNodeMetricsVO.java | 66 + .../org/dubhe/k8s/domain/vo/PtPodsVO.java | 151 + .../org/dubhe/k8s/domain/vo/VolumeVO.java | 60 + .../org/dubhe/k8s/enums/AccessModeEnum.java | 48 + .../enums/BusinessLabelServiceNameEnum.java | 90 + .../dubhe/k8s/enums/GraphicsCardTypeEnum.java | 50 + .../dubhe/k8s/enums/ImagePullPolicyEnum.java | 48 + .../java/org/dubhe/k8s/enums/K8sKindEnum.java | 76 + .../org/dubhe/k8s/enums/K8sResponseEnum.java | 82 + .../dubhe/k8s/enums/K8sTaskStatusEnum.java | 48 + .../k8s/enums/K8sTolerationEffectEnum.java | 48 + .../k8s/enums/K8sTolerationOperatorEnum.java | 44 + .../dubhe/k8s/enums/LackOfResourcesEnum.java | 62 + .../k8s/enums/LimitsOfResourcesEnum.java | 58 + .../k8s/enums/NodeConditionTypeEnum.java | 56 + .../org/dubhe/k8s/enums/PodPhaseEnum.java | 61 + .../dubhe/k8s/enums/PvReclaimPolicyEnum.java | 48 + .../dubhe/k8s/enums/RestartPolicyEnum.java | 48 + .../org/dubhe/k8s/enums/ShellCommandEnum.java | 44 + .../org/dubhe/k8s/enums/TopLogInfoEnum.java | 73 + .../dubhe/k8s/enums/ValidationTypeEnum.java | 29 + .../dubhe/k8s/enums/WatcherActionEnum.java | 63 + .../K8sCallBackPodInterceptor.java | 57 + .../k8s/properties/ClusterProperties.java | 50 + .../DeploymentCallbackAsyncService.java | 35 + .../dubhe/k8s/service/K8sResourceService.java | 83 + .../org/dubhe/k8s/service/K8sTaskService.java | 85 + .../k8s/service/PodCallbackAsyncService.java | 35 + .../org/dubhe/k8s/service/PodService.java | 69 + .../service/impl/K8sResourceServiceImpl.java | 163 + .../k8s/service/impl/K8sTaskServiceImpl.java | 179 + .../k8s/service/impl/PodServiceImpl.java | 277 ++ .../org/dubhe/k8s/utils/BizConvertUtils.java | 268 ++ .../org/dubhe/k8s/utils/K8sCallBackTool.java | 189 ++ .../java/org/dubhe/k8s/utils/K8sNameTool.java | 319 ++ .../java/org/dubhe/k8s/utils/K8sUtils.java | 160 + .../java/org/dubhe/k8s/utils/LabelUtils.java | 140 + .../org/dubhe/k8s/utils/MappingUtils.java | 153 + .../java/org/dubhe/k8s/utils/PodUtil.java | 54 + .../org/dubhe/k8s/utils/PrometheusUtil.java | 102 + .../dubhe/k8s/utils/ResourceBuildUtils.java | 205 ++ .../org/dubhe/k8s/utils/UnitConvertUtils.java | 71 + .../org/dubhe/k8s/utils/ValidationUtils.java | 37 + .../java/org/dubhe/k8s/utils/YamlUtils.java | 120 + .../src/main/resources/kubeconfig_test | 19 + .../main/resources/mapper/K8sTaskMapper.xml | 200 ++ dubhe-server/common-recycle/pom.xml | 49 + .../dubhe/recycle/config/RecycleConfig.java | 72 + .../recycle/config/RecycleMvcConfig.java | 47 + .../recycle/dao/RecycleDetailMapper.java | 27 + .../org/dubhe/recycle/dao/RecycleMapper.java | 27 + .../recycle/domain/dto/RecycleCreateDTO.java | 111 + .../domain/dto/RecycleDetailCreateDTO.java | 82 + .../domain/dto/RecycleTaskDeleteDTO.java | 38 + .../domain/dto/RecycleTaskQueryDTO.java | 52 + .../dubhe/recycle/domain/entity/Recycle.java | 91 + .../recycle/domain/entity/RecycleDetail.java | 80 + .../recycle/enums/RecycleModuleEnum.java | 99 + .../recycle/enums/RecycleResourceEnum.java | 93 + .../recycle/enums/RecycleStatusEnum.java | 57 + .../dubhe/recycle/enums/RecycleTypeEnum.java | 45 + .../recycle/global/AbstractGlobalRecycle.java | 215 ++ .../interceptor/RecycleCallInterceptor.java | 57 + .../recycle/rest/RecycleCallController.java | 70 + .../recycle/service/CustomRecycleService.java | 55 + .../dubhe/recycle/service/RecycleService.java | 58 + .../service/impl/RecycleServiceImpl.java | 165 + .../org/dubhe/recycle/utils/RecycleTool.java | 241 ++ dubhe-server/deploy-base.sh | 44 + .../deploy/cloud/deploy-individual.sh | 41 + dubhe-server/deploy/cloud/deploy.sh | 28 + dubhe-server/deploy/cloud/server-admin.yaml | 78 + dubhe-server/deploy/cloud/server-auth.yaml | 78 + .../deploy/cloud/server-dubhe-algorithm.yaml | 78 + .../deploy/cloud/server-dubhe-data-dcm.yaml | 78 + .../deploy/cloud/server-dubhe-data-task.yaml | 81 + .../deploy/cloud/server-dubhe-data.yaml | 81 + .../deploy/cloud/server-dubhe-image.yaml | 78 + .../deploy/cloud/server-dubhe-k8s.yaml | 81 + .../deploy/cloud/server-dubhe-measure.yaml | 78 + .../deploy/cloud/server-dubhe-model.yaml | 81 + .../deploy/cloud/server-dubhe-notebook.yaml | 81 + .../deploy/cloud/server-dubhe-optimize.yaml | 81 + .../cloud/server-dubhe-serving-gateway.yaml | 82 + .../deploy/cloud/server-dubhe-serving.yaml | 81 + .../deploy/cloud/server-dubhe-train.yaml | 78 + dubhe-server/deploy/cloud/server-dubhe.yaml | 1425 ++++++++ dubhe-server/deploy/cloud/server-gateway.yaml | 79 + dubhe-server/dubhe-algorithm/pom.xml | 125 + .../dubhe/algorithm/AlgorithmApplication.java | 37 + .../async/TrainAlgorithmUploadAsync.java | 119 + .../dubhe/algorithm/client/ImageClient.java | 42 + .../algorithm/client/NoteBookClient.java | 54 + .../client/fallback/ImageClientFallback.java | 37 + .../fallback/NoteBookClientFallback.java | 42 + .../algorithm/config/AlgorithmPoolConfig.java | 80 + .../algorithm/constant/AlgorithmConstant.java | 52 + .../constant/TrainAlgorithmConfig.java | 62 + .../constant/UserAuxiliaryInfoConstant.java | 31 + .../algorithm/dao/PtTrainAlgorithmMapper.java | 70 + .../dao/PtTrainAlgorithmUsageMapper.java | 32 + .../domain/dto/PtModelAlgorithmCreateDTO.java | 51 + .../domain/dto/PtTrainAlgorithmCreateDTO.java | 98 + .../domain/dto/PtTrainAlgorithmDeleteDTO.java | 42 + .../domain/dto/PtTrainAlgorithmQueryDTO.java | 60 + .../domain/dto/PtTrainAlgorithmUpdateDTO.java | 65 + .../dto/PtTrainAlgorithmUsageCreateDTO.java | 50 + .../dto/PtTrainAlgorithmUsageDeleteDTO.java | 40 + .../dto/PtTrainAlgorithmUsageQueryDTO.java | 44 + .../dto/PtTrainAlgorithmUsageUpdateDTO.java | 49 + .../domain/entity/PtTrainAlgorithm.java | 147 + .../domain/entity/PtTrainAlgorithmUsage.java | 68 + .../domain/vo/PtTrainAlgorithmQueryVO.java | 101 + .../vo/PtTrainAlgorithmUsageQueryVO.java | 62 + .../algorithm/enums/AlgorithmSourceEnum.java | 46 + .../algorithm/enums/AlgorithmStatusEnum.java | 63 + .../rest/PtTrainAlgorithmController.java | 124 + .../rest/PtTrainAlgorithmUsageController.java | 84 + .../service/PtTrainAlgorithmService.java | 121 + .../service/PtTrainAlgorithmUsageService.java | 62 + .../AlgorithmRecycleFileServiceImpl.java | 63 + .../impl/PtTrainAlgorithmServiceImpl.java | 640 ++++ .../PtTrainAlgorithmUsageServiceImpl.java | 186 ++ .../org/dubhe/algorithm/utils/TrainUtil.java | 80 + .../src/main/resources/banner.txt | 14 + .../src/main/resources/bootstrap.yml | 37 + .../algorithm/PtTrainAlgorithmUsageTest.java | 96 + .../dubhe/algorithm/TrainAlgorithmTest.java | 99 + dubhe-server/dubhe-data-dcm/lib/LICENSE.txt | 470 +++ .../lib/dcm4che-core-5.19.1.jar | Bin 0 -> 483246 bytes dubhe-server/dubhe-data-dcm/pom.xml | 112 + .../org/dubhe/dcm/DubheDcmApplication.java | 37 + .../org/dubhe/dcm/constant/DcmConstant.java | 83 + .../dubhe/dcm/dao/DataLesionSliceMapper.java | 48 + .../dubhe/dcm/dao/DataMedicineFileMapper.java | 123 + .../org/dubhe/dcm/dao/DataMedicineMapper.java | 89 + .../dcm/domain/dto/DataLesionDrawInfoDTO.java | 39 + .../domain/dto/DataLesionSliceCreateDTO.java | 43 + .../domain/dto/DataLesionSliceDeleteDTO.java | 37 + .../domain/dto/DataLesionSliceUpdateDTO.java | 49 + .../dcm/domain/dto/DataMedcineUpdateDTO.java | 42 + .../dcm/domain/dto/DataMedicineCreateDTO.java | 78 + .../dcm/domain/dto/DataMedicineDeleteDTO.java | 37 + .../domain/dto/DataMedicineFileCreateDTO.java | 44 + .../dcm/domain/dto/DataMedicineImportDTO.java | 43 + .../dcm/domain/dto/DataMedicineQueryDTO.java | 78 + .../dcm/domain/dto/MedicineAnnotationDTO.java | 60 + .../domain/dto/MedicineAutoAnnotationDTO.java | 44 + .../dcm/domain/entity/DataLesionSlice.java | 69 + .../dubhe/dcm/domain/entity/DataMedicine.java | 101 + .../dcm/domain/entity/DataMedicineFile.java | 70 + .../dcm/domain/vo/DataLesionSliceVO.java | 49 + .../vo/DataMedicineCompleteAnnotationVO.java | 41 + .../dubhe/dcm/domain/vo/DataMedicineVO.java | 93 + .../org/dubhe/dcm/domain/vo/ScheduleVO.java | 46 + .../constant/DcmDataStateCodeConstant.java | 49 + .../constant/DcmDataStateMachineConstant.java | 58 + .../constant/DcmFileStateCodeConstant.java | 46 + .../constant/DcmFileStateMachineConstant.java | 52 + .../dcm/machine/enums/DcmDataStateEnum.java | 124 + .../dcm/machine/enums/DcmFileStateEnum.java | 138 + .../machine/proxy/DcmStateMachineProxy.java | 61 + .../state/AbstractDataMedicineState.java | 71 + .../machine/state/AbstractDcmFileState.java | 67 + .../AnnotationCompleteDcmState.java | 73 + .../datamedicine/AnnotationDataState.java | 86 + .../AutoAnnotationCompleteDcmState.java | 72 + .../AutomaticLabelingDcmState.java | 57 + .../datamedicine/NotAnnotationDcmState.java | 85 + .../file/AnnotationCompleteDcmFileState.java | 72 + .../specific/file/AnnotationFileState.java | 87 + .../AutoAnnotationCompleteDcmFileState.java | 74 + .../file/NotAnnotationDcmFileState.java | 102 + .../DcmDataMedicineStateMachine.java | 196 ++ .../statemachine/DcmFileStateMachine.java | 191 ++ .../statemachine/DcmGlobalStateMachine.java | 40 + .../machine/utils/DcmStateMachineUtil.java | 49 + .../dcm/rest/DataLesionSliceController.java | 75 + .../dcm/rest/DataMedicineController.java | 99 + .../rest/MedicineAnnotationController.java | 69 + .../dcm/service/DataLesionSliceService.java | 81 + .../dcm/service/DataMedicineFileService.java | 87 + .../dcm/service/DataMedicineService.java | 149 + .../service/MedicineAnnotationService.java | 71 + .../impl/DataLesionSliceServiceImpl.java | 163 + .../impl/DataMedicineFileServiceImpl.java | 202 ++ .../service/impl/DataMedicineServiceImpl.java | 537 +++ .../impl/MedicineAnnotationServiceImpl.java | 380 +++ .../service/task/DataMedicineRecycleFile.java | 114 + .../src/main/resources/banner.txt | 8 + .../src/main/resources/bootstrap.yml | 50 + .../mapper/DataMedicineFileMapper.xml | 35 + dubhe-server/dubhe-data-task/pom.xml | 146 + .../dubhe/task/DubheDataTaskApplication.java | 39 + .../task/data/AnnotationCopySchedule.java | 50 + .../data/AnnotationQueueExecuteThread.java | 124 + .../task/data/DataTaskExecuteThread.java | 632 ++++ .../task/data/EnhanceQueueExecuteThread.java | 92 + .../org/dubhe/task/data/FileCopySchedule.java | 50 + .../task/data/ImageNetQueueExecuteThread.java | 119 + .../data/MedicineAnnotationExecuteThread.java | 67 + .../task/data/OfRecordQueueExecuteThread.java | 126 + .../TextClassificationQueueExecuteThread.java | 156 + .../task/data/TrackQueueExecuteThread.java | 147 + .../data/VideoSampleQueueExecuteThread.java | 77 + .../org/dubhe/task/util/TableDataUtil.java | 142 + .../src/main/resources/addTask.lua | 5 + .../src/main/resources/banner.txt | 8 + .../src/main/resources/bootstrap.yml | 44 + .../src/main/resources/finishedTask.lua | 4 + .../src/main/resources/restartTask.lua | 4 + .../task/DubheDataTaskApplicationTests.java | 13 + .../org/dubhe/data/DubheDataApplication.java | 37 + .../dubhe/data/client/TrainServerClient.java | 46 + .../client/fallback/TrainServerFallback.java | 45 + .../dubhe/data/config/EsConfiguration.java | 99 + .../config/EsRestClientConfiguration.java | 57 + .../dubhe/data/dao/DataSequenceMapper.java | 59 + .../domain/dto/DatasetConvertPresetDTO.java | 47 + .../data/domain/dto/DatasetCsvImportDTO.java | 45 + .../dubhe/data/domain/dto/EsDataFileDTO.java | 106 + .../dubhe/data/domain/dto/EsTransportDTO.java | 95 + .../domain/dto/GroupConvertPresetDTO.java | 38 + .../dubhe/data/domain/dto/LabelDeleteDTO.java | 40 + .../dubhe/data/domain/dto/LabelUpdateDTO.java | 66 + .../data/domain/entity/DataSequence.java | 46 + .../constant/ErrorMessageConstant.java | 37 + .../state/specific/data/IsTheImportState.java | 66 + .../data/machine/utils/StateIdentifyUtil.java | 129 + .../data/service/DataSequenceService.java | 60 + .../service/impl/DataSequenceServiceImpl.java | 103 + .../service/task/LabelGroupRecycleFile.java | 120 + .../org/dubhe/data/util/GeneratorKeyUtil.java | 88 + .../java/org/dubhe/data/util/IdAlloc.java | 63 + .../data/util/MyPreciseShardingAlgorithm.java | 89 + .../dubhe-data/src/main/resources/banner.txt | 8 + .../src/main/resources/bootstrap.yml | 46 + .../mapper/DatasetGroupLabelMapper.xml | 26 + .../resources/mapper/DatasetLabelMapper.xml | 15 + .../dubhe/data/DubheDataApplicationTests.java | 34 + dubhe-server/dubhe-image/pom.xml | 170 + .../org/dubhe/image/ImageApplication.java | 35 + .../image/async/HarborImagePushAsync.java | 141 + .../org/dubhe/image/dao/PtImageMapper.java | 49 + .../image/domain/dto/PtImageDeleteDTO.java | 39 + .../image/domain/dto/PtImageQueryDTO.java | 52 + .../image/domain/dto/PtImageQueryUrlDTO.java | 45 + .../image/domain/dto/PtImageSmallDTO.java | 40 + .../image/domain/dto/PtImageUpdateDTO.java | 47 + .../image/domain/dto/PtImageUploadDTO.java | 71 + .../dubhe/image/domain/entity/PtImage.java | 92 + .../dubhe/image/domain/vo/PtImageQueryVO.java | 60 + .../dubhe/image/enums/HarborResourceEnum.java | 51 + .../dubhe/image/rest/PtImageController.java | 109 + .../dubhe/image/service/PtImageService.java | 103 + .../service/impl/PtImageServiceImpl.java | 476 +++ .../image/service/task/ImageRecycleFile.java | 77 + .../dubhe-image/src/main/resources/banner.txt | 11 + .../src/main/resources/bootstrap.yml | 36 + .../java/org/dubhe/image/PtImageTest.java | 125 + .../dubhe/dubhek8s/DubheK8sApplication.java | 36 + .../dubhe/dubhek8s/domain/dto/NodeDTO.java | 99 + .../org/dubhe/dubhek8s/domain/dto/PodDTO.java | 57 + .../event/callback/DeploymentCallback.java | 99 + .../dubhek8s/event/callback/PodCallback.java | 190 ++ .../event/watcher/DeploymentWatcher.java | 100 + .../dubhek8s/event/watcher/PodWatcher.java | 102 + .../observer/BatchservingObserver.java | 80 + .../dubhek8s/observer/ModelOptObserver.java | 79 + .../dubhek8s/observer/ServingObserver.java | 75 + .../dubhek8s/observer/TrainJobObserver.java | 62 + .../dubhe/dubhek8s/rest/PodController.java | 105 + .../dubhek8s/rest/SystemNodeController.java | 88 + .../dubhek8s/service/SystemNodeService.java | 65 + .../service/impl/SystemNodeServiceImpl.java | 368 +++ .../dubhek8s/task/DelayCudResourceTask.java | 149 + .../dubhe-k8s/src/main/resources/banner.txt | 10 + .../src/main/resources/bootstrap.yml | 27 + .../dubhe/dubhek8s/k8s/PodCallbackTest.java | 93 + .../dubhe/dubhek8s/k8s/ResourceCacheTest.java | 96 + .../dubhek8s/k8s/api/DeploymentApiTest.java | 91 + .../k8s/api/DistributeTrainApiTest.java | 91 + .../dubhe/dubhek8s/k8s/api/HarborApiTest.java | 71 + .../k8s/api/JupyterResourceApiTest.java | 151 + .../dubhek8s/k8s/api/LimitRangeApiTest.java | 96 + .../k8s/api/LogMonitoringApiTest.java | 97 + .../dubhek8s/k8s/api/MetricsApiTest.java | 80 + .../dubhek8s/k8s/api/ModelOptJobApiTest.java | 91 + .../dubhek8s/k8s/api/ModelServingApiTest.java | 76 + .../dubhek8s/k8s/api/NamespaceApiTest.java | 103 + .../k8s/api/NativeResourceApiTest.java | 51 + .../dubhe/dubhek8s/k8s/api/NodeApiTest.java | 141 + .../k8s/api/PersistentVolumeClaimApiTest.java | 100 + .../dubhe/dubhek8s/k8s/api/PodApiTest.java | 126 + .../k8s/api/ResourceQuotaApiTest.java | 75 + .../dubhek8s/k8s/api/TrainJobApiTest.java | 93 + .../dubhek8s/redis/ResourceCacheTest.java | 96 + dubhe-server/dubhe-measure/pom.xml | 162 + .../org/dubhe/measure/MeasureApplication.java | 35 + .../async/GenerateMeasureFileAsync.java | 111 + .../measure/config/AlgorithmPoolConfig.java | 79 + .../measure/constant/MeasureConstants.java | 44 + .../dubhe/measure/dao/PtMeasureMapper.java | 42 + .../domain/dto/PtMeasureCreateDTO.java | 60 + .../measure/domain/dto/PtMeasureDTO.java | 52 + .../domain/dto/PtMeasureDeleteDTO.java | 44 + .../measure/domain/dto/PtMeasureQueryDTO.java | 39 + .../domain/dto/PtMeasureUpdateDTO.java | 67 + .../measure/domain/entity/PtMeasure.java | 85 + .../measure/domain/vo/PtMeasureQueryVO.java | 64 + .../measure/rest/PtMeasureController.java | 89 + .../measure/service/PtMeasureService.java | 77 + .../service/impl/PtMeasureServiceImpl.java | 365 ++ .../service/task/MeasureRecycleFile.java | 71 + .../src/main/resources/banner.txt | 9 + .../src/main/resources/bootstrap.yml | 36 + .../java/org/dubhe/measure/PtMeasureTest.java | 134 + .../org/dubhe/model/ModelApplication.java | 38 + .../model/client/BatchServingClient.java | 41 + .../client/ModelOptTaskInstanceClient.java | 42 + .../org/dubhe/model/client/ServingClient.java | 40 + .../org/dubhe/model/client/TrainClient.java | 43 + .../fallback/BatchServingClientFallback.java | 33 + .../ModelOptTaskInstanceClientFallback.java | 33 + .../fallback/ServingClientFallback.java | 33 + .../client/fallback/TrainClientFallback.java | 34 + .../dubhe/model/constant/ModelConstants.java | 54 + .../dubhe/model/dao/PtModelBranchMapper.java | 50 + .../dubhe/model/dao/PtModelInfoMapper.java | 60 + .../dubhe/model/dao/PtModelSuffixMapper.java | 27 + .../dubhe/model/dao/PtModelTypeMapper.java | 27 + .../domain/dto/ModelConvertPresetDTO.java | 54 + .../domain/dto/PtModelBranchCreateDTO.java | 65 + .../domain/dto/PtModelBranchDeleteDTO.java | 39 + .../domain/dto/PtModelBranchQueryDTO.java | 42 + .../domain/dto/PtModelBranchUpdateDTO.java | 51 + .../domain/dto/PtModelConvertOnnxDTO.java | 39 + .../domain/dto/PtModelInfoByResourceDTO.java | 47 + .../domain/dto/PtModelInfoCreateDTO.java | 91 + .../domain/dto/PtModelInfoDeleteDTO.java | 39 + .../domain/dto/PtModelInfoPackageDTO.java | 100 + .../model/domain/dto/PtModelInfoQueryDTO.java | 55 + .../domain/dto/PtModelInfoUpdateDTO.java | 68 + .../dto/PtModelOptimizationCreateDTO.java | 47 + .../model/domain/dto/PtModelSuffixDTO.java | 38 + .../model/domain/dto/PtModelTypeQueryDTO.java | 38 + .../model/domain/dto/ServingModelDTO.java | 42 + .../model/domain/entity/PtModelBranch.java | 110 + .../model/domain/entity/PtModelInfo.java | 122 + .../model/domain/entity/PtModelSuffix.java | 56 + .../model/domain/entity/PtModelType.java | 56 + .../model/domain/enums/ModelConvertEnum.java | 66 + .../domain/enums/ModelCopyStatusEnum.java | 53 + .../domain/enums/ModelEnumInterface.java | 30 + .../model/domain/enums/ModelPackageEnum.java | 58 + .../model/domain/enums/ModelResourceEnum.java | 58 + .../domain/vo/PtModelBranchCreateVO.java | 35 + .../domain/vo/PtModelBranchDeleteVO.java | 35 + .../domain/vo/PtModelBranchUpdateVO.java | 35 + .../model/domain/vo/PtModelConvertOnnxVO.java | 34 + .../domain/vo/PtModelInfoByResourceVO.java | 51 + .../model/domain/vo/PtModelInfoCreateVO.java | 35 + .../model/domain/vo/PtModelInfoDeleteVO.java | 35 + .../model/domain/vo/PtModelInfoUpdateVO.java | 35 + .../dubhe/model/pool/ThreadPoolConfig.java | 43 + .../model/rest/PtModelBranchController.java | 95 + .../model/rest/PtModelInfoController.java | 119 + .../model/rest/PtModelSuffixController.java | 49 + .../model/rest/PtModelTypeController.java | 49 + .../org/dubhe/model/service/FileService.java | 74 + .../model/service/PtModelBranchService.java | 107 + .../model/service/PtModelInfoService.java | 117 + .../model/service/PtModelSuffixService.java | 36 + .../model/service/PtModelTypeService.java | 37 + .../ModelRecycleFileServiceImpl.java | 62 + .../model/service/impl/FileServiceImpl.java | 166 + .../impl/PtModelBranchServiceImpl.java | 637 ++++ .../service/impl/PtModelInfoServiceImpl.java | 586 ++++ .../impl/PtModelSuffixServiceImpl.java | 64 + .../service/impl/PtModelTypeServiceImpl.java | 65 + .../model/service/storage/AsyncStorage.java | 0 .../dubhe/model/utils/ModelStatusUtil.java | 112 + .../dubhe-model/src/main/resources/banner.txt | 16 + .../src/main/resources/bootstrap.yml | 35 + .../java/org/dubhe/model/ModelBranchTest.java | 82 + .../java/org/dubhe/model/ModelInfoTest.java | 102 + dubhe-server/dubhe-notebook/pom.xml | 108 + .../notebook/DubheNotebookApplication.java | 38 + .../dubhe/notebook/client/DatasetClient.java | 44 + .../dubhe/notebook/client/ImageClient.java | 43 + .../fallback/DatasetClientFallback.java | 36 + .../client/fallback/ImageClientFallback.java | 35 + .../dubhe/notebook/config/NoteBookConfig.java | 53 + .../notebook/convert/NoteBookConvert.java | 33 + .../convert/PtJupyterResourceConvert.java | 71 + .../dubhe/notebook/dao/NoteBookMapper.java | 93 + .../domain/dto/NoteBookCreateDTO.java | 71 + .../domain/dto/NoteBookListQueryDTO.java | 44 + .../domain/dto/NoteBookStatusDTO.java | 37 + .../dto/NotebookK8sPodCallbackCreateDTO.java | 36 + .../domain/dto/SourceNoteBookDTO.java | 42 + .../notebook/domain/entity/NoteBook.java | 202 ++ .../dubhe/notebook/domain/vo/NoteBookVO.java | 119 + .../notebook/enums/NoteBookStatusEnum.java | 117 + .../rest/K8sCallbackPodController.java | 61 + .../notebook/rest/NoteBookController.java | 164 + .../notebook/service/NoteBookService.java | 209 ++ .../service/ProcessNotebookCommand.java | 54 + .../impl/NoteBookAsyncServiceImpl.java | 87 + .../service/impl/NoteBookServiceImpl.java | 811 +++++ .../task/NoteBookStatusRefreshTask.java | 124 + .../notebook/task/NoteBookUrlRefreshTask.java | 103 + .../dubhe/notebook/utils/NotebookUtil.java | 180 + .../src/main/resources/banner.txt | 11 + .../src/main/resources/bootstrap.yml | 33 + .../dubhe/k8s/utils/K8sCallBackToolTest.java | 50 + .../DubheNotebookApplicationTests.java | 29 + dubhe-server/dubhe-optimize/pom.xml | 124 + .../dubhe/optimize/OptimizeApplication.java | 40 + .../optimize/client/AlgorithmClient.java | 44 + .../org/dubhe/optimize/client/DictClient.java | 43 + .../optimize/client/DictDetailClient.java | 45 + .../optimize/client/ModelInfoClient.java | 44 + .../fallback/AlgorithmClientFallback.java | 41 + .../client/fallback/DictClientFallback.java | 40 + .../fallback/DictDetailClientFallback.java | 40 + .../fallback/ModelInfoClientFallback.java | 41 + .../optimize/constant/ModelOptConstant.java | 147 + .../optimize/dao/ModelOptBuiltInMapper.java | 62 + .../optimize/dao/ModelOptDatasetMapper.java | 37 + .../dao/ModelOptTaskInstanceMapper.java | 51 + .../optimize/dao/ModelOptTaskMapper.java | 41 + .../domain/dto/ModelOptDatasetCreateDTO.java | 42 + .../domain/dto/ModelOptLogQueryDTO.java | 47 + .../domain/dto/ModelOptTaskCreateDTO.java | 99 + .../domain/dto/ModelOptTaskDeleteDTO.java | 37 + .../dto/ModelOptTaskInstanceCancelDTO.java | 37 + .../dto/ModelOptTaskInstanceDeleteDTO.java | 36 + .../dto/ModelOptTaskInstanceDetailDTO.java | 37 + .../dto/ModelOptTaskInstanceQueryDTO.java | 74 + .../dto/ModelOptTaskInstanceResubmitDTO.java | 37 + .../domain/dto/ModelOptTaskQueryDTO.java | 62 + .../domain/dto/ModelOptTaskRunParamDTO.java | 67 + .../domain/dto/ModelOptTaskSubmitDTO.java | 37 + .../domain/dto/ModelOptTaskUpdateDTO.java | 101 + .../ModelOptK8sPodCallbackCreateDTO.java | 35 + .../domain/entity/ModelOptBuiltIn.java | 74 + .../domain/entity/ModelOptDataset.java | 52 + .../optimize/domain/entity/ModelOptTask.java | 134 + .../domain/entity/ModelOptTaskInstance.java | 208 ++ .../domain/vo/ModelOptAlgorithmQueryVO.java | 40 + .../optimize/domain/vo/ModelOptCreateVO.java | 40 + .../domain/vo/ModelOptDatasetQueryVO.java | 36 + .../optimize/domain/vo/ModelOptDatasetVO.java | 51 + .../domain/vo/ModelOptModelQueryVO.java | 37 + .../domain/vo/ModelOptResultQueryVO.java | 57 + .../vo/ModelOptTaskInstanceQueryVO.java | 136 + .../domain/vo/ModelOptTaskQueryVO.java | 79 + .../optimize/domain/vo/ModelOptUpdateVO.java | 39 + .../optimize/enums/DistillCommandEnum.java | 92 + .../optimize/enums/ModelOptErrorEnum.java | 127 + .../enums/ModelOptInstanceStatusEnum.java | 46 + .../optimize/enums/OptimizeTypeEnum.java | 66 + .../rest/K8sCallbackPodController.java | 64 + .../optimize/rest/ModelOptTaskController.java | 127 + .../rest/ModelOptTaskInstanceController.java | 91 + .../service/ModelOptTaskInstanceService.java | 116 + .../optimize/service/ModelOptTaskService.java | 130 + .../impl/ModelOptAsyncServiceImpl.java | 62 + .../impl/ModelOptTaskInstanceServiceImpl.java | 620 ++++ .../service/impl/ModelOptTaskServiceImpl.java | 606 ++++ .../src/main/resources/banner.txt | 13 + .../src/main/resources/bootstrap.yml | 36 + .../mapper/ModelOptBuiltInMapper.xml | 48 + .../ServingGatewayApplication.java | 35 + .../servinggateway/config/CorsConfig.java | 51 + .../CustomErrorWebFluxAutoConfiguration.java | 122 + .../config/GatewayServiceHandler.java | 159 + .../config/JsonErrorWebExceptionHandler.java | 100 + .../config/MetricsGatewayFilterFactory.java | 212 ++ .../RedisRouteDefinitionRepository.java | 89 + .../config/RedisStreamListener.java | 196 ++ .../constant/GatewayConstant.java | 71 + .../dao/GatewayRouteMapper.java | 42 + .../domain/vo/GatewayRouteQueryVO.java | 51 + .../enums/ServingRouteEventEnum.java | 79 + .../filter/AuthValidateFilter.java | 99 + .../service/GatewayRouteService.java | 43 + .../service/impl/GatewayRouteServiceImpl.java | 58 + .../dubhe/servinggateway/utils/AesUtil.java | 98 + .../utils/TokenValidateUtil.java | 74 + .../src/main/resources/banner.txt | 10 + .../src/main/resources/bootstrap.yml | 27 + .../resources/mapper/bootstrap-cloud-dev.yml | 0 .../org/dubhe/serving/ServingApplication.java | 35 + .../dubhe/serving/client/AlgorithmClient.java | 46 + .../org/dubhe/serving/client/ImageClient.java | 43 + .../org/dubhe/serving/client/MailClient.java | 42 + .../serving/client/ModelBranchClient.java | 43 + .../dubhe/serving/client/ModelInfoClient.java | 43 + .../fallback/AlgorithmClientFallback.java | 34 + .../client/fallback/ImageClientFallback.java | 35 + .../client/fallback/MailClientFallback.java | 34 + .../fallback/ModelBranchClientFallback.java | 34 + .../fallback/ModelInfoClientFallback.java | 35 + .../serving/config/ServingPoolConfig.java | 69 + .../serving/config/TrainHarborConfig.java | 39 + .../serving/constant/ServingConstant.java | 122 + .../dubhe/serving/dao/BatchServingMapper.java | 42 + .../dubhe/serving/dao/ServingInfoMapper.java | 43 + .../serving/dao/ServingModelConfigMapper.java | 69 + .../domain/dto/BatchServingCreateDTO.java | 103 + .../domain/dto/BatchServingDeleteDTO.java | 36 + .../domain/dto/BatchServingDetailDTO.java | 37 + .../BatchServingK8sPodCallbackCreateDTO.java | 36 + .../domain/dto/BatchServingQueryDTO.java | 56 + .../domain/dto/BatchServingStartDTO.java | 36 + .../domain/dto/BatchServingStopDTO.java | 36 + .../domain/dto/BatchServingUpdateDTO.java | 114 + .../serving/domain/dto/PredictParamDTO.java | 36 + .../domain/dto/ServingInfoCreateDTO.java | 62 + .../domain/dto/ServingInfoDeleteDTO.java | 36 + .../domain/dto/ServingInfoDetailDTO.java | 35 + .../domain/dto/ServingInfoQueryDTO.java | 56 + .../domain/dto/ServingInfoUpdateDTO.java | 52 + ...ServingK8sDeploymentCallbackCreateDTO.java | 35 + .../dto/ServingK8sPodCallbackCreateDTO.java | 19 + .../domain/dto/ServingLogQueryDTO.java | 52 + .../domain/dto/ServingModelConfigDTO.java | 96 + .../serving/domain/dto/ServingPredictDTO.java | 40 + .../serving/domain/dto/ServingStartDTO.java | 36 + .../serving/domain/dto/ServingStopDTO.java | 37 + .../serving/domain/entity/BatchServing.java | 241 ++ .../dubhe/serving/domain/entity/DataInfo.java | 41 + .../serving/domain/entity/ServingInfo.java | 114 + .../domain/entity/ServingModelConfig.java | 204 ++ .../domain/vo/BatchServingCreateVO.java | 39 + .../domain/vo/BatchServingDeleteVO.java | 36 + .../domain/vo/BatchServingDetailVO.java | 110 + .../domain/vo/BatchServingQueryVO.java | 68 + .../domain/vo/BatchServingStartVO.java | 42 + .../serving/domain/vo/BatchServingStopVO.java | 39 + .../domain/vo/BatchServingUpdateVO.java | 39 + .../serving/domain/vo/PredictParamVO.java | 48 + .../domain/vo/ServingConfigMetricsVO.java | 51 + .../domain/vo/ServingInfoCreateVO.java | 39 + .../domain/vo/ServingInfoDeleteVO.java | 36 + .../domain/vo/ServingInfoDetailVO.java | 70 + .../serving/domain/vo/ServingInfoQueryVO.java | 68 + .../domain/vo/ServingInfoUpdateVO.java | 39 + .../domain/vo/ServingMetricsGrafanaVO.java | 38 + .../serving/domain/vo/ServingMetricsVO.java | 38 + .../domain/vo/ServingModelConfigVO.java | 107 + .../domain/vo/ServingPodMetricsVO.java | 104 + .../serving/domain/vo/ServingPredictVO.java | 36 + .../serving/domain/vo/ServingStartVO.java | 39 + .../serving/domain/vo/ServingStopVO.java | 39 + .../dubhe/serving/enums/ServingErrorEnum.java | 71 + .../serving/enums/ServingFrameTypeEnum.java | 66 + .../serving/enums/ServingRouteEventEnum.java | 78 + .../serving/enums/ServingStatusEnum.java | 49 + .../dubhe/serving/enums/ServingTypeEnum.java | 62 + .../serving/rest/BatchServingController.java | 128 + .../rest/K8sCallbackDeploymentController.java | 66 + .../rest/K8sCallbackPodController.java | 81 + .../dubhe/serving/rest/ServingController.java | 151 + .../serving/service/BatchServingService.java | 129 + .../service/ServingLuaScriptService.java | 51 + .../service/ServingModelConfigService.java | 37 + .../dubhe/serving/service/ServingService.java | 188 ++ .../impl/BatchServingAsyncServiceImpl.java | 56 + .../service/impl/BatchServingServiceImpl.java | 887 +++++ .../ServingDeploymentAsyncServiceImpl.java | 55 + .../impl/ServingLuaScriptServiceImpl.java | 113 + .../impl/ServingModelConfigServiceImpl.java | 43 + .../impl/ServingPodAsyServiceImpl.java | 47 + .../service/impl/ServingServiceImpl.java | 1311 ++++++++ .../serving/task/BatchServingRecycleFile.java | 79 + .../serving/task/DeployServingAsyncTask.java | 550 ++++ .../serving/task/ServingRecycleFile.java | 80 + .../org/dubhe/serving/utils/GrpcClient.java | 194 ++ .../utils/ServingStatusDetailDescUtil.java | 68 + .../src/main/resources/banner.txt | 10 + .../src/main/resources/bootstrap.yml | 35 + .../mapper/ServingModelConfigMapper.xml | 13 + .../src/main/resources/server_prod.crt | 20 + .../src/main/resources/server_test.crt | 21 + dubhe-server/dubhe-train/pom.xml | 124 + .../org/dubhe/train/TrainApplication.java | 37 + .../dubhe/train/async/StopTrainJobAsync.java | 149 + .../org/dubhe/train/async/TrainJobAsync.java | 515 +++ .../train/async/TransactionAsyncManager.java | 73 + .../dubhe/train/client/AlgorithmClient.java | 65 + .../dubhe/train/client/DictDetailClient.java | 44 + .../org/dubhe/train/client/ImageClient.java | 42 + .../dubhe/train/client/ModelBranchClient.java | 43 + .../dubhe/train/client/ModelInfoClient.java | 53 + .../train/client/ResourceSpecsClient.java | 42 + .../fallback/AlgorithmClientFallback.java | 46 + .../fallback/DictDetailClientFallback.java | 33 + .../client/fallback/ImageClientFallback.java | 36 + .../fallback/ModelBranchClientFallback.java | 33 + .../fallback/ModelInfoClientFallback.java | 39 + .../fallback/ResourceSpecsClientFallback.java | 35 + .../dubhe/train/config/TrainHarborConfig.java | 39 + .../dubhe/train/config/TrainJobConfig.java | 107 + .../dubhe/train/config/TrainPoolConfig.java | 81 + .../dubhe/train/constant/TrainConstant.java | 35 + .../org/dubhe/train/dao/PtJobParamMapper.java | 29 + .../org/dubhe/train/dao/PtTrainJobMapper.java | 61 + .../org/dubhe/train/dao/PtTrainMapper.java | 32 + .../dubhe/train/dao/PtTrainParamMapper.java | 32 + .../dubhe/train/domain/dto/BaseImageDTO.java | 44 + .../train/domain/dto/BaseTrainJobDTO.java | 97 + .../train/domain/dto/BaseTrainParamDTO.java | 39 + .../train/domain/dto/PtImageSmallDTO.java | 40 + .../train/domain/dto/PtTrainJobCreateDTO.java | 174 + .../dubhe/train/domain/dto/PtTrainJobDTO.java | 60 + .../train/domain/dto/PtTrainJobDeleteDTO.java | 48 + .../domain/dto/PtTrainJobDetailQueryDTO.java | 44 + .../train/domain/dto/PtTrainJobResumeDTO.java | 49 + .../train/domain/dto/PtTrainJobStopDTO.java | 48 + .../train/domain/dto/PtTrainJobUpdateDTO.java | 154 + .../domain/dto/PtTrainJobVersionQueryDTO.java | 48 + .../train/domain/dto/PtTrainLogQueryDTO.java | 53 + .../train/domain/dto/PtTrainModelDTO.java | 67 + .../domain/dto/PtTrainParamCreateDTO.java | 128 + .../domain/dto/PtTrainParamDeleteDTO.java | 42 + .../domain/dto/PtTrainParamQueryDTO.java | 47 + .../domain/dto/PtTrainParamUpdateDTO.java | 130 + .../train/domain/dto/PtTrainQueryDTO.java | 50 + .../train/domain/dto/VisualTrainQueryDTO.java | 52 + .../AlgorithmK8sPodCallbackCreateDTO.java | 35 + .../dubhe/train/domain/entity/PtJobParam.java | 115 + .../dubhe/train/domain/entity/PtTrain.java | 71 + .../dubhe/train/domain/entity/PtTrainJob.java | 250 ++ .../train/domain/entity/PtTrainParam.java | 186 ++ .../org/dubhe/train/domain/vo/ModelVO.java | 39 + .../domain/vo/PtImageAndAlgorithmVO.java | 57 + .../domain/vo/PtJobMetricsGrafanaVO.java | 39 + .../vo/PtTrainDataSourceStatusQueryVO.java | 45 + .../train/domain/vo/PtTrainJobDeleteVO.java | 40 + .../domain/vo/PtTrainJobDetailQueryVO.java | 196 ++ .../train/domain/vo/PtTrainJobDetailVO.java | 190 ++ .../train/domain/vo/PtTrainJobModelVO.java | 43 + .../domain/vo/PtTrainJobStatisticsMineVO.java | 42 + .../train/domain/vo/PtTrainJobStopVO.java | 40 + .../train/domain/vo/PtTrainLogQueryVO.java | 50 + .../train/domain/vo/PtTrainParamQueryVO.java | 129 + .../org/dubhe/train/domain/vo/PtTrainVO.java | 78 + .../train/domain/vo/VisualTrainQueryVO.java | 70 + .../train/enums/ResourcesPoolTypeEnum.java | 61 + .../dubhe/train/enums/TrainJobStatusEnum.java | 149 + .../org/dubhe/train/enums/TrainTypeEnum.java | 56 + .../train/rest/K8sCallbackPodController.java | 62 + .../train/rest/PtTrainJobController.java | 169 + .../train/rest/PtTrainLogController.java | 75 + .../train/rest/PtTrainParamController.java | 75 + .../train/service/PtTrainJobService.java | 151 + .../train/service/PtTrainLogService.java | 56 + .../train/service/PtTrainParamService.java | 66 + .../impl/AlgorithmAsyncServiceImpl.java | 324 ++ .../service/impl/PtTrainJobServiceImpl.java | 1575 +++++++++ .../service/impl/PtTrainLogServiceImpl.java | 141 + .../service/impl/PtTrainParamServiceImpl.java | 462 +++ .../org/dubhe/train/utils/DubheDateUtil.java | 66 + .../java/org/dubhe/train/utils/ImageUtil.java | 68 + .../java/org/dubhe/train/utils/KeyUtil.java | 39 + .../java/org/dubhe/train/utils/TrainUtil.java | 65 + .../dubhe-train/src/main/resources/banner.txt | 12 + .../src/main/resources/bootstrap.yml | 36 + .../resources/mapper/PtTrainJobMapper.xml | 46 + .../dubhe/train/PtTrainLogServiceTest.java | 54 + .../train/PtTrainModelOptJobApiTest.java | 238 ++ .../org/dubhe/train/TrainParamApiTest.java | 109 + dubhe-server/gateway/pom.xml | 93 + .../org/dubhe/gateway/GatewayApplication.java | 33 + .../org/dubhe/gateway/config/CorsConfig.java | 50 + .../CustomerSwaggerResourceProvider.java | 70 + .../gateway/config/GlobalExceptionConfig.java | 57 + .../gateway/filter/CacheBodyGlobalFilter.java | 86 + .../gateway/handler/JsonExceptionHandler.java | 176 + .../dubhe/gateway/handler/SwaggerHandler.java | 77 + .../gateway/src/main/resources/banner.txt | 12 + .../gateway/src/main/resources/bootstrap.yml | 24 + .../gateway/GatewayApplicationTests.java | 13 + dubhe-server/sql/00-Dubhe-DB.sql | 3 + dubhe-server/sql/01-Dubhe-DDL.sql | 1381 ++++++++ dubhe-server/sql/02-Dubhe-DML.sql | 2930 +++++++++++++++++ dubhe-server/sql/09-Dubhe-Patch.sql | 775 +++++ .../sql/10-Dubhe-Boot-Update-Cloud.sql | 615 ++++ dubhe-server/yaml/README.md | 14 + dubhe-server/yaml/admin.yaml | 44 + dubhe-server/yaml/auth.yaml | 4 + dubhe-server/yaml/common-biz.yaml | 142 + dubhe-server/yaml/common-k8s.yaml | 81 + dubhe-server/yaml/common-recycle.yaml | 36 + dubhe-server/yaml/common-shardingjdbc.yaml | 43 + dubhe-server/yaml/dubhe-algorithm.yaml | 18 + dubhe-server/yaml/dubhe-data-dcm.yaml | 34 + dubhe-server/yaml/dubhe-data-task.yaml | 41 + dubhe-server/yaml/dubhe-data.yaml | 66 + dubhe-server/yaml/dubhe-model.yaml | 13 + dubhe-server/yaml/dubhe-notebook.yaml | 20 + dubhe-server/yaml/dubhe-optimize.yaml | 13 + dubhe-server/yaml/dubhe-serving-gateway.yaml | 48 + dubhe-server/yaml/dubhe-serving.yaml | 24 + dubhe-server/yaml/dubhe-task.yaml | 52 + dubhe-server/yaml/dubhe-train.yaml | 42 + dubhe-server/yaml/gateway.yaml | 128 + dubhe-server/yaml/image.yaml | 53 + dubhe-server/yaml/measure.yaml | 13 + 1250 files changed, 116871 insertions(+) create mode 100644 dubhe-server/HELP.md create mode 100644 dubhe-server/admin/pom.xml create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/AdminApplication.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/client/AuthServiceClient.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/client/fallback/AuthServiceFallback.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/AuthCodeMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DataSequenceMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictDetailMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/LogMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/MenuMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/PermissionMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/ResourceSpecsMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/RoleMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/TeamMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserAvatarMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserGroupMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserRoleMapper.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/MenuProvider.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/RoleProvider.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/TeamProvider.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/UserProvider.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthPermissionUpdDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUserDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/EmailDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ExtConfigDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogSmallDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/NodeDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PodDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleAuthUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleSmallDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUserRoleSmallDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserAvatarUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCenterUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCreateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserEmailUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDeleteDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupUpdDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserPassUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserQueryDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterMailDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserResetPasswordDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRoleUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserStateUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserUpdateDTO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Auth.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/AuthPermission.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DataSequence.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Dict.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DictDetail.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Group.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Log.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Menu.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Permission.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/ResourceSpecs.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Role.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleAuth.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleMenu.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Team.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/TeamUserRole.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/User.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserAvatar.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserRole.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/AuthVO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/EmailVo.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuMetaVo.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuVo.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/PermissionVO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/ResourceSpecsQueryVO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserGroupVO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserVO.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/enums/MenuTypeEnum.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/enums/SystemNodeEnum.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/enums/UserMailCodeEnum.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/event/BaseEvent.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEvent.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventListener.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventPublisher.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/AuthCodeController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictDetailController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ForwardController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LogController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LoginController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MailController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MenuController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/PermissionController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RecycleTaskController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ResourceSpecsController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RoleController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/TeamController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserCenterController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserGroupController.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/AuthCodeService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/DataSequenceService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictDetailService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/LogService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/MailService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/MenuService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/PermissionService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/RecycleTaskService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/ResourceSpecsService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/RoleService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/TeamService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserGroupService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserService.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictDetailConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictSmallConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/LogConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/MenuConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/PermissionConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleSmallConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamSmallConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/UserConvert.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/AuthCodeServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DataSequenceServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictDetailServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/LogServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MailServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MenuServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/PermissionServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RecycleTaskServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/ResourceSpecsServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RoleServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/TeamServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserGroupServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserServiceImpl.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleInvalidResourcesTask.java create mode 100644 dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleResourcesTask.java create mode 100644 dubhe-server/admin/src/main/resources/banner.txt create mode 100644 dubhe-server/admin/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/admin/src/main/resources/mapper/AuthMapper.xml create mode 100644 dubhe-server/admin/src/main/resources/mapper/MenuMapper.xml create mode 100644 dubhe-server/admin/src/main/resources/mapper/PermissionMapper.xml create mode 100644 dubhe-server/admin/src/main/resources/mapper/UserMapper.xml create mode 100644 dubhe-server/admin/src/main/resources/mapper/UserRoleMapper.xml create mode 100644 dubhe-server/admin/src/test/java/org/dubhe/admin/AdminApplicationTests.java create mode 100644 dubhe-server/admin/src/test/java/org/dubhe/admin/DictControllerTest.java create mode 100644 dubhe-server/auth/pom.xml create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/AuthApplication.java create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/config/AuthorizationServerConfig.java create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/config/SecurityConfig.java create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/exception/AuthenticationProviderImpl.java create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthException.java create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthExceptionSerializer.java create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthWebResponseExceptionTranslator.java create mode 100644 dubhe-server/auth/src/main/java/org/dubhe/auth/rest/AuthController.java create mode 100644 dubhe-server/auth/src/main/resources/banner.txt create mode 100644 dubhe-server/auth/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/auth/src/test/java/org/dubhe/auth/AuthApplicationTests.java create mode 100644 dubhe-server/common-biz/base/pom.xml create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/ApiVersion.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/DataPermission.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/EnumValue.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/FlagValidator.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/Log.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ApplicationNameConst.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/AuthConst.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/DataStateCodeConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/FileStateCodeConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/HarborProperties.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MagicNumConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MetaHandlerConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/NumberConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/Permissions.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ResponseCode.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/StringConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/SymbolConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/UserConstant.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/DataContext.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/UserContext.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseImageDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/CommonPermissionDataDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/DictDetailQueryByLabelNameDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/ModelOptAlgorithmCreateDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmQueryDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmUpdateDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/Oauth2TokenDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtDatasetSmallDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtImageQueryUrlDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchConditionQueryDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchQueryByIdDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoConditionQueryDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoQueryByIdDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelStatusQueryDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtStorageSmallDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtTrainDataSourceStatusQueryDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/QueryResourceSpecsDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysPermissionDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysRoleDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamSmallDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllBatchIdDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllByIdDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectByIdDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserSmallDTO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmSourceEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmStatusEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BaseErrorCodeEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BizEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/DatasetTypeEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageSourceEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageStateEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageTypeEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/MeasureStateEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ModelResourceEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/OperationTypeEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ResourcesPoolTypeEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SwitchEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SystemNodeEnum.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/BusinessException.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/CaptchaException.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/DataSequenceException.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/ErrorCode.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/FeignException.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/OAuthResponseError.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/functional/StringFormat.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/service/UserContextService.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/AesUtil.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/DateUtil.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/HttpUtils.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/MathUtils.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/Md5Util.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/NumberUtil.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/PtModelUtil.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RandomUtil.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/ReflectionUtils.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RegexUtil.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RsaEncrypt.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/SpringContextHolder.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/StringUtils.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/TimeTransferUtil.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/BaseVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DataResponseBody.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DatasetVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictDetailVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ModelOptAlgorithmQureyVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ProgressVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelBranchQueryVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelInfoQueryVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/QueryResourceSpecsVO.java create mode 100644 dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/TrainAlgorithmQureyVO.java create mode 100644 dubhe-server/common-biz/data-permission/pom.xml create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/DataPermissionMethod.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/RolePermission.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/PermissionAspect.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/RolePermissionAspect.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/base/BaseService.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MetaHandlerConfig.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MybatisPlusConfig.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/CustomerSqlInterceptor.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/PaginationInterceptor.java create mode 100644 dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/util/SqlUtil.java create mode 100644 dubhe-server/common-biz/data-response/pom.xml create mode 100644 dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/exception/handler/GlobalExceptionHandler.java create mode 100644 dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/factory/DataResponseFactory.java create mode 100644 dubhe-server/common-biz/db/pom.xml create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/annotation/Query.java create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/BaseConvert.java create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/PageQueryBase.java create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/MetaHandlerConstant.java create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/PermissionConstant.java create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/entity/BaseEntity.java create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/PageUtil.java create mode 100644 dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/WrapperHelp.java create mode 100644 dubhe-server/common-biz/file/pom.xml create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/FileStoreApi.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/HostFileStoreApiImpl.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/NfsFileStoreApiImpl.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/ShellFileStoreApiImpl.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/config/NfsConfig.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FileDTO.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FilePageDTO.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/MinioDownloadDTO.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/BizPathEnum.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/CopyTypeEnum.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/exception/NfsBizException.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/DubheFileUtil.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/IOUtil.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/LocalFileUtil.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioUtil.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioWebTokenBody.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsFactory.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsPool.java create mode 100644 dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsUtil.java create mode 100644 dubhe-server/common-biz/log/pom.xml create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/aspect/LogAspect.java create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/entity/LogInfo.java create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/enums/LogEnum.java create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/BaseLogFilter.java create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/ConsoleLogFilter.java create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/GlobalRequestLogFilter.java create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/handler/ScheduleTaskHandler.java create mode 100644 dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/utils/LogUtil.java create mode 100644 dubhe-server/common-biz/log/src/main/resources/logback.xml create mode 100644 dubhe-server/common-biz/pom.xml create mode 100644 dubhe-server/common-biz/redis/pom.xml create mode 100644 dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/config/RedisConfig.java create mode 100644 dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/utils/RedisUtils.java create mode 100644 dubhe-server/common-biz/state-machine/pom.xml create mode 100644 dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/dto/StateChangeDTO.java create mode 100644 dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/exception/StateMachineException.java create mode 100644 dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/utils/StateMachineProxyUtil.java create mode 100644 dubhe-server/common-cloud/auth-config/pom.xml create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/config/ResourceServerConfig.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/decorator/AuthenticationThreadLocalTaskDecorator.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/dto/JwtUserDTO.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerAccessDeniedHandler.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerTokenExceptionEntryPoint.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/AccessTokenConverterFactory.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/PasswordEncoderFactory.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenServicesFactory.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenStoreFactory.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/filter/OAuth2ResponseErrorFilter.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClient.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClientFallback.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminUserService.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/GrantedAuthorityImpl.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/OAuth2UserContextServiceImpl.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/UserDetailsServiceImpl.java create mode 100644 dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/utils/JwtUtils.java create mode 100644 dubhe-server/common-cloud/configuration/pom.xml create mode 100644 dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-data.yml create mode 100644 dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-dev.yml create mode 100644 dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-test.yml create mode 100644 dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-dev.yml create mode 100644 dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-dev.yml create mode 100644 dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-test.yml create mode 100644 dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-prod.yml create mode 100644 dubhe-server/common-cloud/pom.xml create mode 100644 dubhe-server/common-cloud/registration/pom.xml create mode 100644 dubhe-server/common-cloud/registration/src/main/java/org/dubhe/cloud/registration/RegistrationConfig.java create mode 100644 dubhe-server/common-cloud/remote-call/pom.xml create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/FeignErrorDecoder.java create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/HttpClientConfig.java create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RemoteCallConfig.java create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateConfig.java create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateHolder.java create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/FeignInterceptor.java create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/RestTemplateTokenInterceptor.java create mode 100644 dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/strategy/RequestAttributeHystrixConcurrencyStrategy.java create mode 100644 dubhe-server/common-cloud/swagger/pom.xml create mode 100644 dubhe-server/common-cloud/swagger/src/main/java/org/dubhe/cloud/swagger/config/SwaggerConfig.java create mode 100644 dubhe-server/common-cloud/unit-test/pom.xml create mode 100644 dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/base/BaseTest.java create mode 100644 dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/config/UnitTestConfig.java create mode 100644 dubhe-server/common-k8s/.gitignore create mode 100644 dubhe-server/common-k8s/pom.xml create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/HarborApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/impl/HarborApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/config/HarborConfig.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImagePageVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImageVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/utils/HttpClientUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractDeploymentCallback.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractPodCallback.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sField.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sValidation.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DistributeTrainApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DubheDeploymentApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/JupyterResourceApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LimitRangeApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LogMonitoringApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/MetricsApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelOptJobApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelServingApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NamespaceApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NativeResourceApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NodeApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PersistentVolumeClaimApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PodApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceIisolationApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceQuotaApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/TrainJobApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/VolumeApi.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DistributeTrainApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DubheDeploymentApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/JupyterResourceApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LimitRangeApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LogMonitoringApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/MetricsApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelOptJobApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelServingApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NamespaceApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NativeResourceApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NodeApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PersistentVolumeClaimApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PodApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceIisolationApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceQuotaApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/TrainJobApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/VolumeApiImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/aspect/ValidationAspect.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/cache/ResourceCache.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sCallbackMvcConfig.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sConfig.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sNameConfig.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/constant/K8sLabelConstants.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/constant/K8sParamConstants.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/constant/RedisConstants.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/dao/K8sResourceMapper.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/dao/K8sTaskMapper.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/PtBaseResult.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildFsVolumeBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildIngressBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildServiceBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/DistributeTrainBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/K8sTaskBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LabelBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LogMonitoringBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ModelServingBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PrometheusMetricBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtDeploymentBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJobBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterJobBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterResourceBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtLimitRangeBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationDeploymentBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationJobBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtMountDirBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtPersistentVolumeClaimBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtResourceQuotaBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ResourceYamlBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/TaskYamlBO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrain.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainDoneable.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainList.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainSpec.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sDeploymentCallbackCreateDTO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sPodCallbackCreateDTO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/NodeIsolationDTO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogDownloadQueryDTO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogQueryDTO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodQueryDTO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sResource.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sTask.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainer.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateTerminated.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateWaiting.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStatus.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeployment.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeploymentCondition.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrain.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainContainer.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainResources.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizHTTPIngressPath.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngress.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressRule.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressTLS.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIntOrString.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJob.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJobCondition.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRange.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRangeItem.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNamespace.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNode.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeAddress.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeCondition.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeSystemInfo.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaim.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaimCondition.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPod.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodCondition.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodMetrics.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizQuantity.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizResourceQuota.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizScopedResourceSelectorRequirement.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizSecret.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizService.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizTaint.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolume.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolumeMount.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/GpuUsageVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/LogMonitoringVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultValueVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/ModelServingVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodLogQueryVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodRangeMetricsVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtContainerMetricsVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterDeployVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterJobVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtNodeMetricsVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtPodsVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/VolumeVO.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/AccessModeEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/BusinessLabelServiceNameEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/GraphicsCardTypeEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ImagePullPolicyEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sKindEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sResponseEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTaskStatusEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationEffectEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationOperatorEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LackOfResourcesEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LimitsOfResourcesEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/NodeConditionTypeEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PodPhaseEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PvReclaimPolicyEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/RestartPolicyEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ShellCommandEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/TopLogInfoEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ValidationTypeEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/WatcherActionEnum.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/interceptor/K8sCallBackPodInterceptor.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/properties/ClusterProperties.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/DeploymentCallbackAsyncService.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sResourceService.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sTaskService.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodCallbackAsyncService.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodService.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sResourceServiceImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sTaskServiceImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/PodServiceImpl.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/BizConvertUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sCallBackTool.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sNameTool.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/LabelUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/MappingUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PodUtil.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PrometheusUtil.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ResourceBuildUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/UnitConvertUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ValidationUtils.java create mode 100644 dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/YamlUtils.java create mode 100644 dubhe-server/common-k8s/src/main/resources/kubeconfig_test create mode 100644 dubhe-server/common-k8s/src/main/resources/mapper/K8sTaskMapper.xml create mode 100644 dubhe-server/common-recycle/pom.xml create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleConfig.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleMvcConfig.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleDetailMapper.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleMapper.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleCreateDTO.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleDetailCreateDTO.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskDeleteDTO.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskQueryDTO.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/Recycle.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/RecycleDetail.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleModuleEnum.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleResourceEnum.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleStatusEnum.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleTypeEnum.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/global/AbstractGlobalRecycle.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/interceptor/RecycleCallInterceptor.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/rest/RecycleCallController.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/CustomRecycleService.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/RecycleService.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/impl/RecycleServiceImpl.java create mode 100644 dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/utils/RecycleTool.java create mode 100644 dubhe-server/deploy-base.sh create mode 100644 dubhe-server/deploy/cloud/deploy-individual.sh create mode 100644 dubhe-server/deploy/cloud/deploy.sh create mode 100644 dubhe-server/deploy/cloud/server-admin.yaml create mode 100644 dubhe-server/deploy/cloud/server-auth.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-algorithm.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-data-dcm.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-data-task.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-data.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-image.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-k8s.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-measure.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-model.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-notebook.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-optimize.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-serving-gateway.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-serving.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe-train.yaml create mode 100644 dubhe-server/deploy/cloud/server-dubhe.yaml create mode 100644 dubhe-server/deploy/cloud/server-gateway.yaml create mode 100644 dubhe-server/dubhe-algorithm/pom.xml create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/AlgorithmApplication.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/async/TrainAlgorithmUploadAsync.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/ImageClient.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/NoteBookClient.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/ImageClientFallback.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/NoteBookClientFallback.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/config/AlgorithmPoolConfig.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/AlgorithmConstant.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/TrainAlgorithmConfig.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/UserAuxiliaryInfoConstant.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmMapper.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmUsageMapper.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtModelAlgorithmCreateDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmCreateDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmDeleteDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmQueryDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUpdateDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageCreateDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageDeleteDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageQueryDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageUpdateDTO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithm.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithmUsage.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmQueryVO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmUsageQueryVO.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmSourceEnum.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmStatusEnum.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmController.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmUsageController.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmService.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmUsageService.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/RecycleFileService/AlgorithmRecycleFileServiceImpl.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmServiceImpl.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmUsageServiceImpl.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/utils/TrainUtil.java create mode 100644 dubhe-server/dubhe-algorithm/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-algorithm/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/PtTrainAlgorithmUsageTest.java create mode 100644 dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/TrainAlgorithmTest.java create mode 100644 dubhe-server/dubhe-data-dcm/lib/LICENSE.txt create mode 100644 dubhe-server/dubhe-data-dcm/lib/dcm4che-core-5.19.1.jar create mode 100644 dubhe-server/dubhe-data-dcm/pom.xml create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/DubheDcmApplication.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/constant/DcmConstant.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataLesionSliceMapper.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineFileMapper.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineMapper.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionDrawInfoDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceCreateDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceDeleteDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceUpdateDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedcineUpdateDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineCreateDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineDeleteDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineFileCreateDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineImportDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineQueryDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAnnotationDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAutoAnnotationDTO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataLesionSlice.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicine.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicineFile.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataLesionSliceVO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineCompleteAnnotationVO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineVO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/ScheduleVO.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateCodeConstant.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateMachineConstant.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateCodeConstant.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateMachineConstant.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmDataStateEnum.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmFileStateEnum.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/proxy/DcmStateMachineProxy.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDataMedicineState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDcmFileState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationCompleteDcmState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationDataState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutoAnnotationCompleteDcmState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutomaticLabelingDcmState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/NotAnnotationDcmState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationCompleteDcmFileState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationFileState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AutoAnnotationCompleteDcmFileState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/NotAnnotationDcmFileState.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmDataMedicineStateMachine.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmFileStateMachine.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmGlobalStateMachine.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/utils/DcmStateMachineUtil.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataLesionSliceController.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataMedicineController.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/MedicineAnnotationController.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataLesionSliceService.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineFileService.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineService.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/MedicineAnnotationService.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataLesionSliceServiceImpl.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineFileServiceImpl.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineServiceImpl.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/MedicineAnnotationServiceImpl.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/task/DataMedicineRecycleFile.java create mode 100644 dubhe-server/dubhe-data-dcm/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-data-dcm/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-data-dcm/src/main/resources/mapper/DataMedicineFileMapper.xml create mode 100644 dubhe-server/dubhe-data-task/pom.xml create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/DubheDataTaskApplication.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationCopySchedule.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationQueueExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/DataTaskExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/EnhanceQueueExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/FileCopySchedule.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/ImageNetQueueExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/MedicineAnnotationExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/OfRecordQueueExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/TextClassificationQueueExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/TrackQueueExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/VideoSampleQueueExecuteThread.java create mode 100644 dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/util/TableDataUtil.java create mode 100644 dubhe-server/dubhe-data-task/src/main/resources/addTask.lua create mode 100644 dubhe-server/dubhe-data-task/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-data-task/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-data-task/src/main/resources/finishedTask.lua create mode 100644 dubhe-server/dubhe-data-task/src/main/resources/restartTask.lua create mode 100644 dubhe-server/dubhe-data-task/src/test/java/org/dubhe/task/DubheDataTaskApplicationTests.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/DubheDataApplication.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/client/TrainServerClient.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/client/fallback/TrainServerFallback.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/config/EsConfiguration.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/config/EsRestClientConfiguration.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/dao/DataSequenceMapper.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/dto/DatasetConvertPresetDTO.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/dto/DatasetCsvImportDTO.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/dto/EsDataFileDTO.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/dto/EsTransportDTO.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/dto/GroupConvertPresetDTO.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/dto/LabelDeleteDTO.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/dto/LabelUpdateDTO.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/domain/entity/DataSequence.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/machine/constant/ErrorMessageConstant.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/machine/state/specific/data/IsTheImportState.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/machine/utils/StateIdentifyUtil.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/service/DataSequenceService.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/service/impl/DataSequenceServiceImpl.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/service/task/LabelGroupRecycleFile.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/util/GeneratorKeyUtil.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/util/IdAlloc.java create mode 100644 dubhe-server/dubhe-data/src/main/java/org/dubhe/data/util/MyPreciseShardingAlgorithm.java create mode 100644 dubhe-server/dubhe-data/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-data/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-data/src/main/resources/mapper/DatasetGroupLabelMapper.xml create mode 100644 dubhe-server/dubhe-data/src/main/resources/mapper/DatasetLabelMapper.xml create mode 100644 dubhe-server/dubhe-data/src/test/java/org/dubhe/data/DubheDataApplicationTests.java create mode 100644 dubhe-server/dubhe-image/pom.xml create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/ImageApplication.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/async/HarborImagePushAsync.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/dao/PtImageMapper.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/dto/PtImageDeleteDTO.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/dto/PtImageQueryDTO.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/dto/PtImageQueryUrlDTO.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/dto/PtImageSmallDTO.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/dto/PtImageUpdateDTO.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/dto/PtImageUploadDTO.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/entity/PtImage.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/domain/vo/PtImageQueryVO.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/enums/HarborResourceEnum.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/rest/PtImageController.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/service/PtImageService.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/service/impl/PtImageServiceImpl.java create mode 100644 dubhe-server/dubhe-image/src/main/java/org/dubhe/image/service/task/ImageRecycleFile.java create mode 100644 dubhe-server/dubhe-image/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-image/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-image/src/test/java/org/dubhe/image/PtImageTest.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/DubheK8sApplication.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/domain/dto/NodeDTO.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/domain/dto/PodDTO.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/event/callback/DeploymentCallback.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/event/callback/PodCallback.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/event/watcher/DeploymentWatcher.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/event/watcher/PodWatcher.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/observer/BatchservingObserver.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/observer/ModelOptObserver.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/observer/ServingObserver.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/observer/TrainJobObserver.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/rest/PodController.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/rest/SystemNodeController.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/service/SystemNodeService.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/service/impl/SystemNodeServiceImpl.java create mode 100644 dubhe-server/dubhe-k8s/src/main/java/org/dubhe/dubhek8s/task/DelayCudResourceTask.java create mode 100644 dubhe-server/dubhe-k8s/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-k8s/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/PodCallbackTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/ResourceCacheTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/DeploymentApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/DistributeTrainApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/HarborApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/JupyterResourceApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/LimitRangeApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/LogMonitoringApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/MetricsApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/ModelOptJobApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/ModelServingApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/NamespaceApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/NativeResourceApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/NodeApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/PersistentVolumeClaimApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/PodApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/ResourceQuotaApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/k8s/api/TrainJobApiTest.java create mode 100644 dubhe-server/dubhe-k8s/src/test/java/org/dubhe/dubhek8s/redis/ResourceCacheTest.java create mode 100644 dubhe-server/dubhe-measure/pom.xml create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/MeasureApplication.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/async/GenerateMeasureFileAsync.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/config/AlgorithmPoolConfig.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/constant/MeasureConstants.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/dao/PtMeasureMapper.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/domain/dto/PtMeasureCreateDTO.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/domain/dto/PtMeasureDTO.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/domain/dto/PtMeasureDeleteDTO.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/domain/dto/PtMeasureQueryDTO.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/domain/dto/PtMeasureUpdateDTO.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/domain/entity/PtMeasure.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/domain/vo/PtMeasureQueryVO.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/rest/PtMeasureController.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/service/PtMeasureService.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/service/impl/PtMeasureServiceImpl.java create mode 100644 dubhe-server/dubhe-measure/src/main/java/org/dubhe/measure/service/task/MeasureRecycleFile.java create mode 100644 dubhe-server/dubhe-measure/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-measure/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-measure/src/test/java/org/dubhe/measure/PtMeasureTest.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/ModelApplication.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/BatchServingClient.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/ModelOptTaskInstanceClient.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/ServingClient.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/TrainClient.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/fallback/BatchServingClientFallback.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/fallback/ModelOptTaskInstanceClientFallback.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/fallback/ServingClientFallback.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/client/fallback/TrainClientFallback.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/constant/ModelConstants.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/dao/PtModelBranchMapper.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/dao/PtModelInfoMapper.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/dao/PtModelSuffixMapper.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/dao/PtModelTypeMapper.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/ModelConvertPresetDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelBranchCreateDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelBranchDeleteDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelBranchQueryDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelBranchUpdateDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelConvertOnnxDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelInfoByResourceDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelInfoCreateDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelInfoDeleteDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelInfoPackageDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelInfoQueryDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelInfoUpdateDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelOptimizationCreateDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelSuffixDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/PtModelTypeQueryDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/dto/ServingModelDTO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/entity/PtModelBranch.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/entity/PtModelInfo.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/entity/PtModelSuffix.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/entity/PtModelType.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/enums/ModelConvertEnum.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/enums/ModelCopyStatusEnum.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/enums/ModelEnumInterface.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/enums/ModelPackageEnum.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/enums/ModelResourceEnum.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelBranchCreateVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelBranchDeleteVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelBranchUpdateVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelConvertOnnxVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelInfoByResourceVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelInfoCreateVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelInfoDeleteVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/domain/vo/PtModelInfoUpdateVO.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/pool/ThreadPoolConfig.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/rest/PtModelBranchController.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/rest/PtModelInfoController.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/rest/PtModelSuffixController.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/rest/PtModelTypeController.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/FileService.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/PtModelBranchService.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/PtModelInfoService.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/PtModelSuffixService.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/PtModelTypeService.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/RecycleFileService/ModelRecycleFileServiceImpl.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/impl/FileServiceImpl.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/impl/PtModelBranchServiceImpl.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/impl/PtModelInfoServiceImpl.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/impl/PtModelSuffixServiceImpl.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/impl/PtModelTypeServiceImpl.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/service/storage/AsyncStorage.java create mode 100644 dubhe-server/dubhe-model/src/main/java/org/dubhe/model/utils/ModelStatusUtil.java create mode 100644 dubhe-server/dubhe-model/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-model/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-model/src/test/java/org/dubhe/model/ModelBranchTest.java create mode 100644 dubhe-server/dubhe-model/src/test/java/org/dubhe/model/ModelInfoTest.java create mode 100644 dubhe-server/dubhe-notebook/pom.xml create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/DubheNotebookApplication.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/client/DatasetClient.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/client/ImageClient.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/client/fallback/DatasetClientFallback.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/client/fallback/ImageClientFallback.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/config/NoteBookConfig.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/convert/NoteBookConvert.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/convert/PtJupyterResourceConvert.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/dao/NoteBookMapper.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/domain/dto/NoteBookCreateDTO.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/domain/dto/NoteBookListQueryDTO.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/domain/dto/NoteBookStatusDTO.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/domain/dto/NotebookK8sPodCallbackCreateDTO.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/domain/dto/SourceNoteBookDTO.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/domain/entity/NoteBook.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/domain/vo/NoteBookVO.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/enums/NoteBookStatusEnum.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/rest/K8sCallbackPodController.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/rest/NoteBookController.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/service/NoteBookService.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/service/ProcessNotebookCommand.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/service/impl/NoteBookAsyncServiceImpl.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/service/impl/NoteBookServiceImpl.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/task/NoteBookStatusRefreshTask.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/task/NoteBookUrlRefreshTask.java create mode 100644 dubhe-server/dubhe-notebook/src/main/java/org/dubhe/notebook/utils/NotebookUtil.java create mode 100644 dubhe-server/dubhe-notebook/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-notebook/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-notebook/src/test/java/org/dubhe/k8s/utils/K8sCallBackToolTest.java create mode 100644 dubhe-server/dubhe-notebook/src/test/java/org/dubhe/notebook/DubheNotebookApplicationTests.java create mode 100644 dubhe-server/dubhe-optimize/pom.xml create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/OptimizeApplication.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/AlgorithmClient.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/DictClient.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/DictDetailClient.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/ModelInfoClient.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/fallback/AlgorithmClientFallback.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/fallback/DictClientFallback.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/fallback/DictDetailClientFallback.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/client/fallback/ModelInfoClientFallback.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/constant/ModelOptConstant.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/dao/ModelOptBuiltInMapper.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/dao/ModelOptDatasetMapper.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/dao/ModelOptTaskInstanceMapper.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/dao/ModelOptTaskMapper.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptDatasetCreateDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptLogQueryDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskCreateDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskDeleteDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskInstanceCancelDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskInstanceDeleteDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskInstanceDetailDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskInstanceQueryDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskInstanceResubmitDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskQueryDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskRunParamDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskSubmitDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/ModelOptTaskUpdateDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/dto/callback/ModelOptK8sPodCallbackCreateDTO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/entity/ModelOptBuiltIn.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/entity/ModelOptDataset.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/entity/ModelOptTask.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/entity/ModelOptTaskInstance.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptAlgorithmQueryVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptCreateVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptDatasetQueryVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptDatasetVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptModelQueryVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptResultQueryVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptTaskInstanceQueryVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptTaskQueryVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/domain/vo/ModelOptUpdateVO.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/enums/DistillCommandEnum.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/enums/ModelOptErrorEnum.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/enums/ModelOptInstanceStatusEnum.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/enums/OptimizeTypeEnum.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/rest/K8sCallbackPodController.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/rest/ModelOptTaskController.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/rest/ModelOptTaskInstanceController.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/service/ModelOptTaskInstanceService.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/service/ModelOptTaskService.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/service/impl/ModelOptAsyncServiceImpl.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/service/impl/ModelOptTaskInstanceServiceImpl.java create mode 100644 dubhe-server/dubhe-optimize/src/main/java/org/dubhe/optimize/service/impl/ModelOptTaskServiceImpl.java create mode 100644 dubhe-server/dubhe-optimize/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-optimize/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-optimize/src/main/resources/mapper/ModelOptBuiltInMapper.xml create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/ServingGatewayApplication.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/config/CorsConfig.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/config/CustomErrorWebFluxAutoConfiguration.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/config/GatewayServiceHandler.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/config/JsonErrorWebExceptionHandler.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/config/MetricsGatewayFilterFactory.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/config/RedisRouteDefinitionRepository.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/config/RedisStreamListener.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/constant/GatewayConstant.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/dao/GatewayRouteMapper.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/domain/vo/GatewayRouteQueryVO.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/enums/ServingRouteEventEnum.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/filter/AuthValidateFilter.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/service/GatewayRouteService.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/service/impl/GatewayRouteServiceImpl.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/utils/AesUtil.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/java/org/dubhe/servinggateway/utils/TokenValidateUtil.java create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-serving-gateway/src/main/resources/mapper/bootstrap-cloud-dev.yml create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/ServingApplication.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/AlgorithmClient.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/ImageClient.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/MailClient.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/ModelBranchClient.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/ModelInfoClient.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/fallback/AlgorithmClientFallback.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/fallback/ImageClientFallback.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/fallback/MailClientFallback.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/fallback/ModelBranchClientFallback.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/client/fallback/ModelInfoClientFallback.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/config/ServingPoolConfig.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/config/TrainHarborConfig.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/constant/ServingConstant.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/dao/BatchServingMapper.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/dao/ServingInfoMapper.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/dao/ServingModelConfigMapper.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingCreateDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingDeleteDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingDetailDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingK8sPodCallbackCreateDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingQueryDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingStartDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingStopDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/BatchServingUpdateDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/PredictParamDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingInfoCreateDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingInfoDeleteDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingInfoDetailDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingInfoQueryDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingInfoUpdateDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingK8sDeploymentCallbackCreateDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingK8sPodCallbackCreateDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingLogQueryDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingModelConfigDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingPredictDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingStartDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/dto/ServingStopDTO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/entity/BatchServing.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/entity/DataInfo.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/entity/ServingInfo.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/entity/ServingModelConfig.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/BatchServingCreateVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/BatchServingDeleteVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/BatchServingDetailVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/BatchServingQueryVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/BatchServingStartVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/BatchServingStopVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/BatchServingUpdateVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/PredictParamVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingConfigMetricsVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingInfoCreateVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingInfoDeleteVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingInfoDetailVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingInfoQueryVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingInfoUpdateVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingMetricsGrafanaVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingMetricsVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingModelConfigVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingPodMetricsVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingPredictVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingStartVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/domain/vo/ServingStopVO.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/enums/ServingErrorEnum.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/enums/ServingFrameTypeEnum.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/enums/ServingRouteEventEnum.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/enums/ServingStatusEnum.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/enums/ServingTypeEnum.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/rest/BatchServingController.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/rest/K8sCallbackDeploymentController.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/rest/K8sCallbackPodController.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/rest/ServingController.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/BatchServingService.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/ServingLuaScriptService.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/ServingModelConfigService.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/ServingService.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/impl/BatchServingAsyncServiceImpl.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/impl/BatchServingServiceImpl.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/impl/ServingDeploymentAsyncServiceImpl.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/impl/ServingLuaScriptServiceImpl.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/impl/ServingModelConfigServiceImpl.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/impl/ServingPodAsyServiceImpl.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/service/impl/ServingServiceImpl.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/task/BatchServingRecycleFile.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/task/DeployServingAsyncTask.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/task/ServingRecycleFile.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/utils/GrpcClient.java create mode 100644 dubhe-server/dubhe-serving/src/main/java/org/dubhe/serving/utils/ServingStatusDetailDescUtil.java create mode 100644 dubhe-server/dubhe-serving/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-serving/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-serving/src/main/resources/mapper/ServingModelConfigMapper.xml create mode 100644 dubhe-server/dubhe-serving/src/main/resources/server_prod.crt create mode 100644 dubhe-server/dubhe-serving/src/main/resources/server_test.crt create mode 100644 dubhe-server/dubhe-train/pom.xml create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/TrainApplication.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/async/StopTrainJobAsync.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/async/TrainJobAsync.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/async/TransactionAsyncManager.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/AlgorithmClient.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/DictDetailClient.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/ImageClient.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/ModelBranchClient.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/ModelInfoClient.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/ResourceSpecsClient.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/fallback/AlgorithmClientFallback.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/fallback/DictDetailClientFallback.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/fallback/ImageClientFallback.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/fallback/ModelBranchClientFallback.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/fallback/ModelInfoClientFallback.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/client/fallback/ResourceSpecsClientFallback.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/config/TrainHarborConfig.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/config/TrainJobConfig.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/config/TrainPoolConfig.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/constant/TrainConstant.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/dao/PtJobParamMapper.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/dao/PtTrainJobMapper.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/dao/PtTrainMapper.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/dao/PtTrainParamMapper.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/BaseImageDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/BaseTrainJobDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/BaseTrainParamDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtImageSmallDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobCreateDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobDeleteDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobDetailQueryDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobResumeDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobStopDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobUpdateDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainJobVersionQueryDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainLogQueryDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainModelDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainParamCreateDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainParamDeleteDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainParamQueryDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainParamUpdateDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/PtTrainQueryDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/VisualTrainQueryDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/dto/callback/AlgorithmK8sPodCallbackCreateDTO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/entity/PtJobParam.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/entity/PtTrain.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/entity/PtTrainJob.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/entity/PtTrainParam.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/ModelVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtImageAndAlgorithmVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtJobMetricsGrafanaVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainDataSourceStatusQueryVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainJobDeleteVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainJobDetailQueryVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainJobDetailVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainJobModelVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainJobStatisticsMineVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainJobStopVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainLogQueryVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainParamQueryVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/PtTrainVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/domain/vo/VisualTrainQueryVO.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/enums/ResourcesPoolTypeEnum.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/enums/TrainJobStatusEnum.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/enums/TrainTypeEnum.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/rest/K8sCallbackPodController.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/rest/PtTrainJobController.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/rest/PtTrainLogController.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/rest/PtTrainParamController.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/service/PtTrainJobService.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/service/PtTrainLogService.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/service/PtTrainParamService.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/service/impl/AlgorithmAsyncServiceImpl.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/service/impl/PtTrainJobServiceImpl.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/service/impl/PtTrainLogServiceImpl.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/service/impl/PtTrainParamServiceImpl.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/utils/DubheDateUtil.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/utils/ImageUtil.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/utils/KeyUtil.java create mode 100644 dubhe-server/dubhe-train/src/main/java/org/dubhe/train/utils/TrainUtil.java create mode 100644 dubhe-server/dubhe-train/src/main/resources/banner.txt create mode 100644 dubhe-server/dubhe-train/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/dubhe-train/src/main/resources/mapper/PtTrainJobMapper.xml create mode 100644 dubhe-server/dubhe-train/src/test/java/org/dubhe/train/PtTrainLogServiceTest.java create mode 100644 dubhe-server/dubhe-train/src/test/java/org/dubhe/train/PtTrainModelOptJobApiTest.java create mode 100644 dubhe-server/dubhe-train/src/test/java/org/dubhe/train/TrainParamApiTest.java create mode 100644 dubhe-server/gateway/pom.xml create mode 100644 dubhe-server/gateway/src/main/java/org/dubhe/gateway/GatewayApplication.java create mode 100644 dubhe-server/gateway/src/main/java/org/dubhe/gateway/config/CorsConfig.java create mode 100644 dubhe-server/gateway/src/main/java/org/dubhe/gateway/config/CustomerSwaggerResourceProvider.java create mode 100644 dubhe-server/gateway/src/main/java/org/dubhe/gateway/config/GlobalExceptionConfig.java create mode 100644 dubhe-server/gateway/src/main/java/org/dubhe/gateway/filter/CacheBodyGlobalFilter.java create mode 100644 dubhe-server/gateway/src/main/java/org/dubhe/gateway/handler/JsonExceptionHandler.java create mode 100644 dubhe-server/gateway/src/main/java/org/dubhe/gateway/handler/SwaggerHandler.java create mode 100644 dubhe-server/gateway/src/main/resources/banner.txt create mode 100644 dubhe-server/gateway/src/main/resources/bootstrap.yml create mode 100644 dubhe-server/gateway/src/test/java/org/dubhe/gateway/GatewayApplicationTests.java create mode 100644 dubhe-server/sql/00-Dubhe-DB.sql create mode 100644 dubhe-server/sql/01-Dubhe-DDL.sql create mode 100644 dubhe-server/sql/02-Dubhe-DML.sql create mode 100644 dubhe-server/sql/09-Dubhe-Patch.sql create mode 100644 dubhe-server/sql/10-Dubhe-Boot-Update-Cloud.sql create mode 100644 dubhe-server/yaml/README.md create mode 100644 dubhe-server/yaml/admin.yaml create mode 100644 dubhe-server/yaml/auth.yaml create mode 100644 dubhe-server/yaml/common-biz.yaml create mode 100644 dubhe-server/yaml/common-k8s.yaml create mode 100644 dubhe-server/yaml/common-recycle.yaml create mode 100644 dubhe-server/yaml/common-shardingjdbc.yaml create mode 100644 dubhe-server/yaml/dubhe-algorithm.yaml create mode 100644 dubhe-server/yaml/dubhe-data-dcm.yaml create mode 100644 dubhe-server/yaml/dubhe-data-task.yaml create mode 100644 dubhe-server/yaml/dubhe-data.yaml create mode 100644 dubhe-server/yaml/dubhe-model.yaml create mode 100644 dubhe-server/yaml/dubhe-notebook.yaml create mode 100644 dubhe-server/yaml/dubhe-optimize.yaml create mode 100644 dubhe-server/yaml/dubhe-serving-gateway.yaml create mode 100644 dubhe-server/yaml/dubhe-serving.yaml create mode 100644 dubhe-server/yaml/dubhe-task.yaml create mode 100644 dubhe-server/yaml/dubhe-train.yaml create mode 100644 dubhe-server/yaml/gateway.yaml create mode 100644 dubhe-server/yaml/image.yaml create mode 100644 dubhe-server/yaml/measure.yaml diff --git a/dubhe-server/HELP.md b/dubhe-server/HELP.md new file mode 100644 index 0000000..ee42ad0 --- /dev/null +++ b/dubhe-server/HELP.md @@ -0,0 +1,9 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.4.0/maven-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.4.0/maven-plugin/reference/html/#build-image) + diff --git a/dubhe-server/admin/pom.xml b/dubhe-server/admin/pom.xml new file mode 100644 index 0000000..f57082d --- /dev/null +++ b/dubhe-server/admin/pom.xml @@ -0,0 +1,154 @@ + + + + server + org.dubhe + 0.0.1-SNAPSHOT + + 4.0.0 + + admin + Admin 系统服务 + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + org.dubhe.biz + file + ${org.dubhe.biz.file.version} + + + org.dubhe.biz + data-permission + ${org.dubhe.biz.data-permission.version} + + + org.dubhe.biz + redis + ${org.dubhe.biz.redis.version} + + + + org.dubhe.cloud + registration + ${org.dubhe.cloud.registration.version} + + + + org.dubhe.cloud + configuration + ${org.dubhe.cloud.configuration.version} + + + + org.dubhe.cloud + swagger + ${org.dubhe.cloud.swagger.version} + + + + org.dubhe.biz + data-response + ${org.dubhe.biz.data-response.version} + + + + org.dubhe.cloud + auth-config + ${org.dubhe.cloud.auth-config.version} + + + + org.dubhe.cloud + remote-call + ${org.dubhe.cloud.remote-call.version} + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + + com.github.whvcse + easy-captcha + ${easy-captcha.version} + + + + org.dubhe + common-recycle + ${org.dubhe.common-recycle.version} + + + + org.dubhe.cloud + unit-test + ${org.dubhe.cloud.unit-test.version} + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + true + exec + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + true + src/main/resources + + + + \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/AdminApplication.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/AdminApplication.java new file mode 100644 index 0000000..d4abdf7 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/AdminApplication.java @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * @description Admin启动类 + * @date 2020-12-02 + */ +@SpringBootApplication(scanBasePackages = "org.dubhe") +@MapperScan(basePackages = {"org.dubhe.**.dao"}) +public class AdminApplication { + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/client/AuthServiceClient.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/client/AuthServiceClient.java new file mode 100644 index 0000000..ae26325 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/client/AuthServiceClient.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.client; + +import org.dubhe.admin.client.fallback.AuthServiceFallback; +import org.dubhe.biz.base.constant.ApplicationNameConst; +import org.dubhe.biz.base.dto.Oauth2TokenDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Map; + +/** + * @description feign调用demo + * @date 2020-11-04 + */ +@FeignClient(value = ApplicationNameConst.SERVER_AUTHORIZATION,fallback = AuthServiceFallback.class) +public interface AuthServiceClient { + + /** + * 获取token + * + * @param parameters 获取token请求map + * @return token 信息 + */ + @PostMapping(value = "/oauth/token") + DataResponseBody postAccessToken(@RequestParam Map parameters); + + /** + * 登出 + * @param accessToken token + * @return + */ + @DeleteMapping(value="/oauth/logout") + DataResponseBody logout(@RequestParam("token")String accessToken); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/client/fallback/AuthServiceFallback.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/client/fallback/AuthServiceFallback.java new file mode 100644 index 0000000..5aa0c78 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/client/fallback/AuthServiceFallback.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.client.fallback; + +import org.dubhe.admin.client.AuthServiceClient; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @description Feign 熔断处理类 + * @date 2020-11-04 + */ +@Component +public class AuthServiceFallback implements AuthServiceClient { + + + /** + * 获取token + * + * @param parameters 获取token请求map + * @return token 信息 + */ + @Override + public DataResponseBody postAccessToken(Map parameters) { + return DataResponseFactory.failed("call auth server postAccessToken error "); + } + + /** + * 退出登录 + * + * @param accessToken token + * @return + */ + @Override + public DataResponseBody logout(String accessToken) { + return DataResponseFactory.failed("call auth server logout error "); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/AuthCodeMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/AuthCodeMapper.java new file mode 100644 index 0000000..f93973d --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/AuthCodeMapper.java @@ -0,0 +1,105 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.mapping.FetchType; +import org.dubhe.admin.domain.entity.*; + +import java.util.List; +import java.util.Set; + +/** + * @description 权限组mapper接口 + * @date 2021-05-14 + */ +public interface AuthCodeMapper extends BaseMapper { + + + /** + * 根据角色id查询对应的权限组 + * + * @param queryWrapper role的wrapper对象 + * @return 角色集合 + */ + @Select("select * from role ${ew.customSqlSegment}") + @Results(id = "roleMapperResults", + value = { + @Result(property = "id", column = "id"), + @Result(property = "auths", + column = "id", + many = @Many(select = "org.dubhe.admin.dao.AuthCodeMapper.selectByRoleId", fetchType = FetchType.LAZY))}) + List selectCollList(@Param("ew") Wrapper queryWrapper); + + + /** + * 给权限组绑定具体的权限 + * + * @param list 权限列表 + */ + void tiedWithPermission(List list); + + /** + * 清空指定权限组的权限 + * + * @param authId 权限组id + */ + @Delete("delete from auth_permission where auth_id=#{authId} ") + void untiedWithPermission(Long authId); + + @Delete("") + void untiedByPermissionId(@Param("ids") Set ids); + + /** + * 绑定角色权限 + * + * @param roleAuths 角色权限关联DTO + */ + void tiedRoleAuth(List roleAuths); + + /** + * 根据角色ID解绑角色权限 + * + * @param roleId 角色ID + */ + @Update("delete from roles_auth where role_id = #{roleId}") + void untiedRoleAuthByRoleId(Long roleId); + + /** + * 通过权限组id获取权限 + * + * @param authId 权限组id + * @return List 权限列表 + */ + @Select("select * from permission where id in (select permission_id from auth_permission where auth_id=#{authId}) and deleted=0 order by id") + List getPermissionByAuthId(Long authId); + + /** + * 根据角色id获取绑定的权限组列表 + * + * @param roleId 角色id + * @return 权限组列表 + */ + @Select("select * from auth where id in (select auth_id from roles_auth where role_id=#{roleId}) and deleted=0") + List selectByRoleId(long roleId); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DataSequenceMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DataSequenceMapper.java new file mode 100644 index 0000000..5930a41 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DataSequenceMapper.java @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.dubhe.admin.domain.entity.DataSequence; + +/** + * @description 数据序列 Mapper + * @date 2020-09-23 + */ +public interface DataSequenceMapper extends BaseMapper { + /** + * 根据业务编码查询序列 + * @param businessCode 业务编码 + * @return DataSequence 序号 + */ + @Select("select id, business_code ,start, step from data_sequence where business_code = #{businessCode} for update") + DataSequence selectByBusiness(String businessCode); + + /** + * 根据业务编码更新序列起始值 + * @param businessCode 业务编码 + * @return DataSequence 序号 + */ + @Update("update data_sequence set start = start + step where business_code = #{businessCode} ") + int updateStartByBusinessCode(String businessCode); + + /** + * 查询存在表的记录数 + * @param tableName 表名 + * @return int 查询表记录数 + */ + @Select("select count(1) from ${tableName}") + int checkTableExist(@Param("tableName") String tableName); + + /** + * 执行创建表 + * @param tableName 新表表名 + * @param oldTableName 模板表表名 + */ + @Update({"CREATE TABLE ${tableName} like ${oldTableName}"}) + void createNewTable(@Param("tableName") String tableName, @Param("oldTableName") String oldTableName); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictDetailMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictDetailMapper.java new file mode 100644 index 0000000..c63f0b5 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictDetailMapper.java @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.dubhe.admin.domain.entity.DictDetail; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 字典详情 mapper + * @date 2020-03-26 + */ +public interface DictDetailMapper extends BaseMapper { + /** + * 根据字典ID查找 + * + * @param dictId + * @return + */ + @Select("select * from dict_detail where dict_id =#{dictId} order by sort") + List selectByDictId(Serializable dictId); + + /** + * 根据字典ID和标签查找 + * + * @param dictId + * @param label + * @return + */ + @Select("select * from dict_detail where dict_id=#{dictId} and label=#{label}") + DictDetail selectByDictIdAndLabel(Serializable dictId, String label); + + /** + * 根据字典ID删除 + * + * @param dictId + * @return + */ + @Update("delete from dict_detail where dict_id =#{dictId}") + int deleteByDictId(Serializable dictId); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictMapper.java new file mode 100644 index 0000000..587f3da --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/DictMapper.java @@ -0,0 +1,81 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.mapping.FetchType; +import org.dubhe.admin.domain.entity.Dict; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 字典 mapper + * @date 2020-03-26 + */ +public interface DictMapper extends BaseMapper { + /** + * 查询实体及关联对象 + * + * @param queryWrapper 字典wrapper对象 + * @return 字典列表 + */ + @Select("select * from dict ${ew.customSqlSegment}") + @Results(id = "dictMapperResults", + value = { + @Result(property = "id", column = "id"), + @Result(property = "dictDetails", + column = "id", + many = @Many(select = "org.dubhe.admin.dao.DictDetailMapper.selectByDictId", + fetchType = FetchType.LAZY))}) + List selectCollList(@Param("ew") Wrapper queryWrapper); + + /** + * 分页查询实体及关联对象 + * + * @param page 分页对象 + * @param queryWrapper 字典wrapper对象 + * @return 分页字典集合 + */ + @Select("select * from dict ${ew.customSqlSegment}") + @ResultMap(value = "dictMapperResults") + IPage selectCollPage(Page page, @Param("ew") Wrapper queryWrapper); + + /** + * 根据ID查询实体及关联对象 + * + * @param id 序列id + * @return 字典对象 + */ + @Select("select * from dict where id=#{id}") + @ResultMap("dictMapperResults") + Dict selectCollById(Serializable id); + + /** + * 根据Name查询实体及关联对象 + * + * @param name 字典名称 + * @return 字典对象 + */ + @Select("select * from dict where name=#{name}") + @ResultMap("dictMapperResults") + Dict selectCollByName(String name); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/LogMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/LogMapper.java new file mode 100644 index 0000000..7c45cf1 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/LogMapper.java @@ -0,0 +1,45 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Select; +import org.dubhe.admin.domain.entity.Log; + +/** + * @description 获取一个时间段的IP记录 + * @date 2020-03-25 + */ +public interface LogMapper extends BaseMapper { + /** + * 获取一个时间段的IP记录 + * + * @param date1 startTime + * @param date2 entTime + * @return IP数目 + */ + @Select("select count(*) FROM (select request_ip FROM log where create_time between #{date1} and #{date2} GROUP BY request_ip) as s") + Long findIp(String date1, String date2); + + /** + * 根据日志类型删除信息 + * + * @param logType 日志类型 + */ + @Delete("delete from log where log_type = #{logType}") + void deleteByLogType(String logType); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/MenuMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/MenuMapper.java new file mode 100644 index 0000000..7233fee --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/MenuMapper.java @@ -0,0 +1,83 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.SelectProvider; +import org.dubhe.admin.dao.provider.MenuProvider; +import org.dubhe.admin.domain.entity.Menu; +import org.dubhe.biz.base.dto.SysPermissionDTO; + +import java.util.List; +import java.util.Set; + +/** + * @description 菜单 mapper + * @date 2020-04-02 + */ +public interface MenuMapper extends BaseMapper { + + + /** + * 根据角色id查询用户权限 + * + * @param roleIds 用户id + * @return 权限信息 + */ + List selectPermissionByRoleIds(@Param("list") List roleIds); + + + /** + * 根据组件名称查询 + * + * @param name 组件名称 + * @return 菜单对象 + */ + @Select("select * from menu where component=#{name} and deleted = 0 ") + Menu findByComponentName(String name); + + /** + * 根据菜单的 PID 查询 + * + * @param pid 菜单pid + * @return 菜单列表 + */ + @Select("select * from menu where pid=#{pid} and deleted = 0 ") + List findByPid(long pid); + + /** + * 根据角色ID与菜单类型查询菜单 + * + * @param roleIds roleIDs + * @param type 类型 + * @return 菜单列表 + */ + @SelectProvider(type = MenuProvider.class, method = "findByRolesIdInAndTypeNotOrderBySortAsc") + List findByRolesIdInAndTypeNotOrderBySortAsc(Set roleIds, int type); + + + /** + * 根据角色ID与查询菜单 + * + * @param roleId 角色id + * @return 菜单列表 + */ + @Select("select m.* from menu m,roles_menus rm where m.id= rm.menu_id and rm.role_id=#{roleId} and deleted = 0 ") + List selectByRoleId(long roleId); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/PermissionMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/PermissionMapper.java new file mode 100644 index 0000000..78241e7 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/PermissionMapper.java @@ -0,0 +1,111 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Many; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Result; +import org.apache.ibatis.annotations.Results; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.apache.ibatis.mapping.FetchType; +import org.dubhe.admin.domain.entity.Permission; +import org.dubhe.admin.domain.entity.Role; +import org.dubhe.biz.base.dto.SysPermissionDTO; + +import java.util.List; + +/** + * @description 角色权限关联mapper + * @date 2021-04-26 + */ +public interface PermissionMapper extends BaseMapper { + + + /** + * 查询实体及关联对象 + * + * @param queryWrapper 角色wrapper对象 + * @return 角色列表 + */ + @Select("select * from role ${ew.customSqlSegment}") + @Results(id = "roleMapperResults", + value = { + @Result(property = "id", column = "id"), + @Result(property = "Permissions", + column = "id", + many = @Many(select = "org.dubhe.admin.dao.PermissionMapper.selectByRoleId", fetchType = FetchType.LAZY))}) + List selectCollList(@Param("ew") Wrapper queryWrapper); + + + /** + * 绑定角色权限 + * + * @param roleId 角色ID + * @param menuId 权限ID + */ + @Update("insert into roles_auth values (#{roleId}, #{menuId})") + void tiedRoleAuth(Long roleId, Long menuId); + + + /** + * 根据roleId查询权限列表 + * + * @param roleId roleId + * @return List 权限列表 + */ + @Select("select p.permission, p.name from permission p left join auth_permission ap on p.id = ap.permission_id left join roles_auth ra on ap.auth_id = ra.auth_id where ra.role_id=#{roleId} and deleted=0 ") + List selectByRoleId(long roleId); + + /** + * 根据权限的 PID 查询 + * + * @param pid 父权限id + * @return List 权限列表 + */ + @Select("select * from permission where pid=#{pid} and deleted = 0 ") + List findByPid(long pid); + + + /** + * 根据角色id查询用户权限 + * + * @param roleIds 用户id + * @return 权限信息 + */ + List selectPermissinByRoleIds(@Param("list") List roleIds); + + /** + * 根据权限名获取权限信息 + * + * @param name 权限名称 + * @return 权限 + */ + @Select("select * from permission where name=#{name} and deleted=0") + Permission findByName(String name); + + +} + + + + + + + diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/ResourceSpecsMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/ResourceSpecsMapper.java new file mode 100644 index 0000000..b580aea --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/ResourceSpecsMapper.java @@ -0,0 +1,27 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.dubhe.admin.domain.entity.ResourceSpecs; + +/** + * @description 资源规格管理mapper接口 + * @date 2021-05-27 + */ +public interface ResourceSpecsMapper extends BaseMapper { +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/RoleMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/RoleMapper.java new file mode 100644 index 0000000..9cae87b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/RoleMapper.java @@ -0,0 +1,163 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.mapping.FetchType; +import org.dubhe.admin.dao.provider.RoleProvider; +import org.dubhe.admin.domain.entity.Role; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 角色 mapper + * @date 2020-11-26 + */ +public interface RoleMapper extends BaseMapper { + + /** + * 根据用户id查询用户角色 + * + * @param userId 用户id + * @return 用户信息 + */ + @Select("select sr.id, sr.name, sur.user_id from users_roles sur left join role sr on sr.id = sur.role_id where sur.user_id = #{userId} and sr.deleted = 0 ") + List selectRoleByUserId(@Param("userId") Long userId); + + + /** + * 查询实体及关联对象 + * + * @param queryWrapper 角色wrapper对象 + * @return List 角色列表 + */ + @Select("select * from role ${ew.customSqlSegment}") + @Results(id = "roleMapperResults", + value = { + @Result(property = "id", column = "id"), + @Result(property = "menus", + column = "id", + many = @Many(select = "org.dubhe.admin.dao.MenuMapper.selectByRoleId", fetchType = FetchType.LAZY)), + @Result(property = "auths", + column = "id", + many = @Many(select = "org.dubhe.admin.dao.AuthCodeMapper.selectByRoleId", fetchType = FetchType.LAZY))}) + List selectCollList(@Param("ew") Wrapper queryWrapper); + + /** + * 分页查询实体及关联对象 + * + * @param page 分页对象 + * @param queryWrapper 角色wrapper对象 + * @return IPage 分页角色集合 + */ + @Select("select * from role ${ew.customSqlSegment}") + @ResultMap(value = "roleMapperResults") + IPage selectCollPage(Page page, @Param("ew") Wrapper queryWrapper); + + /** + * 根据ID查询实体及关联对象 + * + * @param id 序列id + * @return 角色 + */ + @Select("select * from role where id=#{id} and deleted = 0 ") + @ResultMap("roleMapperResults") + Role selectCollById(Serializable id); + + /** + * 根据名称查询 + * + * @param name 角色名称 + * @return 角色 + */ + @Select("select * from role where name = #{name} and deleted = 0 ") + Role findByName(String name); + + /** + * 绑定用户角色 + * + * @param userId 用户ID + * @param roleId 角色ID + */ + @Update("insert into users_roles values (#{userId}, #{roleId})") + void tiedUserRole(Long userId, Long roleId); + + /** + * 根据用户ID解绑用户角色 + * + * @param userId 用户ID + */ + @Update("delete from users_roles where user_id = #{userId}") + void untiedUserRoleByUserId(Long userId); + + /** + * 根据角色ID解绑用户角色 + * + * @param roleId 角色ID + */ + @Update("delete from users_roles where role_id = #{roleId}") + void untiedUserRoleByRoleId(Long roleId); + + /** + * 绑定角色菜单 + * + * @param roleId 角色ID + * @param menuId 菜单ID + */ + @Update("insert into roles_menus values (#{roleId}, #{menuId})") + void tiedRoleMenu(Long roleId, Long menuId); + + /** + * 根据角色ID解绑角色菜单 + * + * @param roleId 角色ID + */ + @Update("delete from roles_menus where role_id = #{roleId}") + void untiedRoleMenuByRoleId(Long roleId); + + /** + * 根据菜单ID解绑角色菜单 + * + * @param menuId 菜单ID + */ + @Update("delete from roles_menus where menu_id = #{menuId}") + void untiedRoleMenuByMenuId(Long menuId); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return List 角色列表 + */ + @SelectProvider(type = RoleProvider.class, method = "findRolesByUserId") + List findRolesByUserId(Long userId); + + /** + * 根据团队ID和用户ID查询角色 + * + * @param userId 用户ID + * @param teamId 团队ID + * @return List 角色列表 + */ + @SelectProvider(type = RoleProvider.class, method = "findByUserIdAndTeamId") + List findByUserIdAndTeamId(Long userId, Long teamId); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/TeamMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/TeamMapper.java new file mode 100644 index 0000000..f29f7b2 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/TeamMapper.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.mapping.FetchType; +import org.dubhe.admin.dao.provider.TeamProvider; +import org.dubhe.admin.domain.entity.Team; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 团队 mapper + * @date 2020-03-25 + */ +public interface TeamMapper extends BaseMapper { + + /** + * 根据ID查询名称 + * + * @param userId 用户id + * @return List 团队列表 + */ + @SelectProvider(type = TeamProvider.class, method = "findByUserId") + List findByUserId(Long userId); + + + /** + * 根据ID查询团队实体及关联对象 + * + * @param id 序列id + * @return 团队 + */ + @Select("select * from team where id=#{id}") + @Results(id = "teamMapperResults", + value = { + @Result(column = "id", property = "teamUserList", + many = @Many(select = "org.dubhe.admin.dao.UserMapper.findByTeamId", + fetchType = FetchType.LAZY))}) + Team selectCollById(Serializable id); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserAvatarMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserAvatarMapper.java new file mode 100644 index 0000000..66f4ed9 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserAvatarMapper.java @@ -0,0 +1,26 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.dubhe.admin.domain.entity.UserAvatar; + +/** + * @description 用户头像 mapper + * @date 2020-03-25 + */ +public interface UserAvatarMapper extends BaseMapper { +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserGroupMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserGroupMapper.java new file mode 100644 index 0000000..639bc2e --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserGroupMapper.java @@ -0,0 +1,86 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.dubhe.admin.domain.entity.Group; +import org.dubhe.admin.domain.entity.User; + +import java.util.List; + +/** + * @description 用户组mapper接口 + * @date 2021-05-08 + */ +public interface UserGroupMapper extends BaseMapper { + + /** + * 获取用户组成员信息 + * + * @param groupId 用户组id + * @return List 用户列表 + */ + @Select("select u.* from user u left join user_group gu on u.id = gu.user_id where gu.group_id=#{groupId} and deleted=0 ") + List queryUserByGroupId(Long groupId); + + /** + * 删除用户组 + * + * @param groupId 用户组id + */ + @Update("update pt_group set deleted=1 where id=#{groupId} ") + void delUserGroupByGroupId(Long groupId); + + /** + * 清空用户组成员 + * + * @param groupId 用户组id + */ + @Delete("delete from user_group where group_id=#{groupId} ") + void delUserByGroupId(Long groupId); + + /** + * 新增用户组成员 + * + * @param groupId 用户组id + * @param userId 用户id + */ + @Insert("insert into user_group values (#{groupId},#{userId})") + void addUserWithGroup(Long groupId, Long userId); + + /** + * 删除用户组成员 + * + * @param groupId 用户组id + * @param userId 用户id + */ + @Delete("delete from user_group where group_id=#{groupId} and user_id=#{userId}") + void delUserWithGroup(Long groupId, Long userId); + + /** + * 获取还未分组的用户 + * + * @return List 用户组成员列表 + */ + @Select("select * from user where id not in (select user_id from user_group) and deleted=0") + List findUserWithOutGroup(); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserMapper.java new file mode 100644 index 0000000..cff32e5 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserMapper.java @@ -0,0 +1,147 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Many; +import org.apache.ibatis.annotations.One; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Result; +import org.apache.ibatis.annotations.ResultMap; +import org.apache.ibatis.annotations.Results; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.apache.ibatis.mapping.FetchType; +import org.dubhe.admin.domain.entity.User; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * @description Demo服务mapper + * @date 2020-11-26 + */ +public interface UserMapper extends BaseMapper { + + + /** + * 根据ID查询实体及关联对象 + * + * @param id 用户id + * @return 用户 + */ + @Select("select * from user where id=#{id} and deleted = 0") + @Results(id = "userMapperResults", + value = { + @Result(property = "id", column = "id"), + @Result(property = "roles", + column = "id", + many = @Many(select = "org.dubhe.admin.dao.RoleMapper.findRolesByUserId", + fetchType = FetchType.LAZY)), + @Result(property = "userAvatar", + column = "avatar_id", + one = @One(select = "org.dubhe.admin.dao.UserAvatarMapper.selectById", + fetchType = FetchType.LAZY))}) + User selectCollById(Long id); + + /** + * 根据用户名查询 + * + * @param username 用户名 + * @return 用户 + */ + @Select("select * from user where username=#{username} and deleted = 0") + @ResultMap(value = "userMapperResults") + User findByUsername(String username); + + /** + * 根据邮箱查询 + * + * @param email 邮箱 + * @return 用户 + */ + @Select("select * from user where email=#{email} and deleted = 0") + @ResultMap(value = "userMapperResults") + User findByEmail(String email); + + /** + * 修改密码 + * + * @param username 用户名 + * @param pass 密码 + * @param lastPasswordResetTime 密码最后一次重置时间 + */ + + @Update("update user set password = #{pass} , last_password_reset_time = #{lastPasswordResetTime} where username = #{username}") + void updatePass(String username, String pass, Date lastPasswordResetTime); + + /** + * 修改邮箱 + * + * @param username 用户名 + * @param email 邮箱 + */ + @Update("update user set email = #{email} where username = #{username}") + void updateEmail(String username, String email); + + /** + * 查找用户权限 + * + * @param userId 用户id + * @return 权限集合 + */ + Set queryPermissionByUserId(Long userId); + + + /** + * 查询实体及关联对象 + * + * @param queryWrapper 用户wrapper对象 + * @return 用户集合 + */ + @Select("select * from user ${ew.customSqlSegment}") + @ResultMap(value = "userMapperResults") + List selectCollList(@Param("ew") Wrapper queryWrapper); + + /** + * 分页查询实体及关联对象 + * + * @param page 分页对象 + * @param queryWrapper 用户wrapper对象 + * @return 分页user集合 + */ + @Select("select * from user ${ew.customSqlSegment} order by id desc") + @ResultMap(value = "userMapperResults") + IPage selectCollPage(Page page, @Param("ew") Wrapper queryWrapper); + + /** + * 根据角色分页查询实体及关联对象 + * + * @param page 分页对象 + * @param queryWrapper 用户wrapper对象 + * @param roleId 角色id + * @return 分页用户集合 + */ + @Select("select u.* from user u join users_roles ur on u.id=ur.user_id and ur.role_id=#{roleId} ${ew.customSqlSegment} order by u.id desc") + @ResultMap(value = "userMapperResults") + IPage selectCollPageByRoleId(Page page, @Param("ew") Wrapper queryWrapper, Long roleId); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserRoleMapper.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserRoleMapper.java new file mode 100644 index 0000000..e5e7494 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/UserRoleMapper.java @@ -0,0 +1,46 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.dubhe.admin.domain.entity.UserRole; + +import java.util.List; +import java.util.Set; + +/** + * @description 用户角色 mapper + * @date 2020-6-9 + */ +public interface UserRoleMapper extends BaseMapper { + + /** + * 批量删除用户 + * + * @param userIds 用户id集合 + */ + void deleteByUserId(@Param("list") Set userIds); + + /** + * 批量添加用户角色 + * + * @param userRoles 用户角色实体集合 + */ + void insertBatchs(List userRoles); + +} + diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/MenuProvider.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/MenuProvider.java new file mode 100644 index 0000000..e531ba6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/MenuProvider.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.dao.provider; + +import org.apache.ibatis.jdbc.SQL; + +import java.util.Map; +import java.util.Set; + +/** + * @description 菜单sql构建类 + * @date 2020-04-02 + */ +public class MenuProvider { + public String findByRolesIdInAndTypeNotOrderBySortAsc(Map para) { + Set roleIds = (Set) para.get("roleIds"); + int type = (int) para.get("type"); + StringBuffer roleIdsql = new StringBuffer("rm.role_id in (-1 "); + roleIds.forEach(id -> { + roleIdsql.append("," + id.toString()); + }); + roleIdsql.append(" )"); + return new SQL() {{ + SELECT_DISTINCT("m.*"); + FROM("menu m, roles_menus rm "); + WHERE(roleIdsql + " and m.id=rm.menu_id and m.deleted = 0 and m.type<>" + type); + ORDER_BY("pid, sort, id"); + }}.toString(); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/RoleProvider.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/RoleProvider.java new file mode 100644 index 0000000..1509781 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/RoleProvider.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.dao.provider; + +import java.util.Map; + +/** + * @description 角色构建类 + * @date 2020-04-15 + */ +public class RoleProvider { + public String findRolesByUserId(Long userId) { + StringBuffer sql = new StringBuffer("select r.* from role r, users_roles ur "); + sql.append(" where ur.user_id=#{userId} "); + sql.append(" and ur.role_id=r.id"); + sql.append(" and r.deleted = 0"); + return sql.toString(); + } + + public String findByUserIdAndTeamId(Map para) { + StringBuffer sql = new StringBuffer("select r.* from role r, teams_users_roles tur "); + sql.append(" where tur.user_id=#{userId} "); + sql.append(" and tur.team_id=#{teamId} "); + sql.append(" and tur.role_id=r.id"); + sql.append(" and r.deleted = 0"); + return sql.toString(); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/TeamProvider.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/TeamProvider.java new file mode 100644 index 0000000..0523de1 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/TeamProvider.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.dao.provider; + +/** + * @description 团队构建类 + * @date 2020-04-15 + */ +public class TeamProvider { + + public String findByUserId(Long userId) { + StringBuffer sql = new StringBuffer("select t.* from team t, teams_users_roles tur "); + sql.append(" where tur.user_id=#{userId} "); + sql.append(" and tur.team_id=t.id "); + return sql.toString(); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/UserProvider.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/UserProvider.java new file mode 100644 index 0000000..b3a44a6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/dao/provider/UserProvider.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.dao.provider; + +/** + * @description 用户sql构建类 + * @date 2020-04-02 + */ +public class UserProvider { + public String queryPermissionByUserId(Long userId) { + StringBuffer sql = new StringBuffer("select m.permission from menu m, users_roles ur, roles_menus rm "); + sql.append(" where ur.user_id = #{userId} and ur.role_id = rm.role_id and rm.menu_id = m.id and m.permission <> '' and m.deleted = 0 "); + return sql.toString(); + } + + public String findPermissionByUserIdAndTeamId(Long userId, Long teamId) { + StringBuffer sql = new StringBuffer("select m.permission from menu m, teams_users_roles tur ,roles_menus rm "); + sql.append(" where tur.user_id=#{userId} "); + sql.append(" and tur.role_id=rm.role_id "); + sql.append(" and tur.team_id=#{team_id} "); + sql.append(" and rm.menu_id=m.id"); + sql.append(" and and m.deleted = 0 "); + return sql.toString(); + } + + public String findByTeamId(Long teamId) { + StringBuffer sql = new StringBuffer("select u.* from user u,teams_users_roles tur where tur.team_id=#{teamId} and tur.user_id=u.id and u.deleted = 0"); + return sql.toString(); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeCreateDTO.java new file mode 100644 index 0000000..a7fb30f --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeCreateDTO.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 创建权限组DTO + * @date 2021-05-14 + */ +@Data +public class AuthCodeCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "autoCode", required = true) + @NotEmpty(message = "权限组code不能为空") + private String authCode; + + @ApiModelProperty(value = "权限id集合", required = true) + @NotNull(message = "权限不能为空") + private Set permissions; + + @ApiModelProperty("描述") + private String description; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeDeleteDTO.java new file mode 100644 index 0000000..41812d5 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeDeleteDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 删除权限组DTO + * @date 2021-05-17 + */ +@Data +public class AuthCodeDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull(message = "id不能为空") + private Set ids; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeQueryDTO.java new file mode 100644 index 0000000..b514437 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeQueryDTO.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.biz.db.base.PageQueryBase; + +import java.io.Serializable; + +/** + * @description 分页查看权限组列表 + * @date 2021-05-14 + */ + +@Data +public class AuthCodeQueryDTO extends PageQueryBase implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "权限组名称") + private String authCode; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeUpdateDTO.java new file mode 100644 index 0000000..2f58d6d --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthCodeUpdateDTO.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 修改权限组DTO + * @date 2021-05-14 + */ +@Data +public class AuthCodeUpdateDTO implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + @ApiModelProperty(value = "autoCode", required = true) + @NotEmpty(message = "权限组code不能为空") + private String authCode; + + @ApiModelProperty(value = "权限id集合") + private Set permissions; + + @ApiModelProperty("描述") + private String description; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthPermissionUpdDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthPermissionUpdDTO.java new file mode 100644 index 0000000..758a896 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthPermissionUpdDTO.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 修改权限组权限DTO + * @date 2021-05-14 + */ +@Data +public class AuthPermissionUpdDTO implements Serializable { + + @ApiModelProperty(value = "权限组id") + @NotNull(message = "权限组id不能为空") + private Long authId; + + @ApiModelProperty(value = "权限id集合") + private Set permissionIds; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUpdateDTO.java new file mode 100644 index 0000000..4c6a55b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUpdateDTO.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.admin.domain.entity.Permission; + +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; + +/** + * @description 修改操作权限DTO + * @date 2021-04-28 + */ +@Data +public class AuthUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + @ApiModelProperty(value = "操作权限名称") + @Size(max = 255, message = "名称长度不能超过255") + private String name; + + @ApiModelProperty(value = "操作权限标识") + @Size(max = 255, message = "默认权限长度不能超过255") + private String permission; + + + private Set permissions; + + private Boolean deleted; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AuthUpdateDTO role = (AuthUpdateDTO) o; + return Objects.equals(id, role.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUserDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUserDTO.java new file mode 100644 index 0000000..eea9ae8 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/AuthUserDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * @description 用户登录dto + * @date 2020-06-01 + */ +@Getter +@Setter +public class AuthUserDTO implements Serializable { + + private static final long serialVersionUID = 6243696246160576285L; + @NotBlank + private String username; + + @NotBlank + private String password; + + private String code; + + private String uuid = ""; + + @Override + public String toString() { + return "{username=" + username + ", password= ******}"; + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictCreateDTO.java new file mode 100644 index 0000000..57805b7 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictCreateDTO.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import org.dubhe.admin.domain.entity.DictDetail; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 字典新增DTO + * @date 2020-06-01 + */ +@Data +public class DictCreateDTO implements Serializable { + + private static final long serialVersionUID = -901581636964448858L; + + @NotBlank(message = "字典名称不能为空") + @Length(max = 255, message = "名称长度不能超过255") + private String name; + + @Length(max = 255, message = "备注长度不能超过255") + private String remark; + + private Timestamp createTime; + + @TableField(exist = false) + private List dictDetails; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDTO.java new file mode 100644 index 0000000..d00654f --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDTO.java @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; +/** + * @description 字典DTO + * @date 2020-06-01 + */ +@Data +public class DictDTO implements Serializable { + + private static final long serialVersionUID = 1L; + private Long id; + + private String name; + + private String remark; + + private List dictDetails; + + private Timestamp createTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDeleteDTO.java new file mode 100644 index 0000000..7692168 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDeleteDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 字典删除DTO + * @date 2020-06-01 + */ +@Data +public class DictDeleteDTO implements Serializable { + + private static final long serialVersionUID = 6346677566514471535L; + + @NotEmpty(message = "id不能为空") + Set ids; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailCreateDTO.java new file mode 100644 index 0000000..10decae --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailCreateDTO.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 字典详情修改DTO + * @date 2020-06-29 + */ +@Data +public class DictDetailCreateDTO implements Serializable { + + private static final long serialVersionUID = -1936563127368448645L; + + @ApiModelProperty(value = "字典标签") + @Length(max = 255, message = "字典标签长度不能超过255") + private String label; + + @ApiModelProperty(value = "字典值") + @Length(max = 255, message = "字典值长度不能超过255") + private String value; + + @ApiModelProperty(value = "排序") + private String sort = "999"; + + private Long dictId; + + private Timestamp createTime; + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDTO.java new file mode 100644 index 0000000..d9e2984 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDTO.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 字典详情DTO + * @date 2020-06-01 + */ +@Data +public class DictDetailDTO implements Serializable { + + private static final long serialVersionUID = 1521993584428225098L; + + @ApiModelProperty(value = "字典详情id") + private Long id; + + @ApiModelProperty(value = "字典label") + private String label; + + @ApiModelProperty(value = "字典详情value") + private String value; + + @ApiModelProperty(value = "排序") + private String sort; + + @ApiModelProperty(value = "字典id") + private Long dictId; + + private Timestamp createTime; + + private Timestamp updateTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDeleteDTO.java new file mode 100644 index 0000000..a3db766 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailDeleteDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 字典详情删除DTO + * @date 2020-06-29 + */ +@Data +public class DictDetailDeleteDTO implements Serializable { + + private static final long serialVersionUID = -151060582445500836L; + + @NotEmpty(message = "id不能为空") + private Set ids; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailQueryDTO.java new file mode 100644 index 0000000..2e22718 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailQueryDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.biz.db.annotation.Query; + +import java.io.Serializable; + +/** + * @description 字典详情查询实体 + * @date 2020-06-01 + */ +@Data +public class DictDetailQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + @Query(type = Query.Type.LIKE) + private String label; + + @Query(propName = "dict_id", type = Query.Type.EQ) + private Long dictId; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailUpdateDTO.java new file mode 100644 index 0000000..c0721f9 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictDetailUpdateDTO.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.admin.domain.entity.DictDetail; +import org.hibernate.validator.constraints.Length; + + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 字典详情修改DTO + * @date 2020-06-29 + */ +@Data +public class DictDetailUpdateDTO implements Serializable { + + private static final long serialVersionUID = -1936563127368448645L; + + @NotNull(groups = DictDetail.Update.class) + private Long id; + + @ApiModelProperty(value = "字典标签") + @Length(max = 255, message = "字典标签长度不能超过255") + private String label; + + @ApiModelProperty(value = "字典值") + @Length(max = 255, message = "字典值长度不能超过255") + private String value; + + @ApiModelProperty(value = "排序") + private String sort = "999"; + + @ApiModelProperty(value = "字典id") + private Long dictId; + + private Timestamp createTime; + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictQueryDTO.java new file mode 100644 index 0000000..a608d66 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictQueryDTO.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.biz.db.annotation.Query; + +import java.io.Serializable; + +/** + * @description 公共查询类 + * @date 2020-05-10 + */ +@Data +public class DictQueryDTO implements Serializable { + + private static final long serialVersionUID = -8221913871799888949L; + @Query(blurry = "name,remark") + private String blurry; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallDTO.java new file mode 100644 index 0000000..be1fb62 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallDTO.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 实体类 + * @date 2020-03-16 + */ +@Data +public class DictSmallDTO implements Serializable { + private Long id; + private String name; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallQueryDTO.java new file mode 100644 index 0000000..3369dcc --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictSmallQueryDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 字典查询转换DTO + * @date 2020-06-01 + */ +@Data +public class DictSmallQueryDTO implements Serializable { + private static final long serialVersionUID = 5825111154262768118L; + private Long id; + private String name; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictUpdateDTO.java new file mode 100644 index 0000000..9241c22 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/DictUpdateDTO.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import org.dubhe.admin.domain.entity.Dict; +import org.dubhe.admin.domain.entity.DictDetail; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 字典新增DTO + * @date 2020-06-01 + */ +@Data +public class DictUpdateDTO implements Serializable { + + private static final long serialVersionUID = -901581636964448858L; + + @NotNull(groups = Dict.Update.class) + private Long id; + + @NotBlank + @Length(max = 255, message = "名称长度不能超过255") + private String name; + + @Length(max = 255, message = "备注长度不能超过255") + private String remark; + + private Timestamp createTime; + + @TableField(exist = false) + private List dictDetails; + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/EmailDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/EmailDTO.java new file mode 100644 index 0000000..8b202a7 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/EmailDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description 邮件DTO + * @date 2020-06-01 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EmailDTO { + + @ApiModelProperty(value = "邮箱地址") + String receiverMailAddress; + + @ApiModelProperty(value = "标题") + String subject; + + @ApiModelProperty(value = "验证码") + String code; + + @ApiModelProperty(value = "类型 1 用户注册 2 修改邮箱 3 其他") + Integer type; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ExtConfigDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ExtConfigDTO.java new file mode 100644 index 0000000..30dd9dd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ExtConfigDTO.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import java.io.Serializable; + +/** + * @description 扩展配置DTO + * @date 2021-01-25 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExtConfigDTO implements Serializable { + + @ApiModelProperty(value = "返回上一级菜单") + private String backTo; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogDTO.java new file mode 100644 index 0000000..a66b662 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogDTO.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 日志转换DTO + * @date 2020-06-01 + */ +@Data +public class LogDTO implements Serializable { + + private static final long serialVersionUID = 1L; + private Long id; + + private String username; + + private String description; + + private String method; + + private String params; + + private String browser; + + private String requestIp; + + private String address; + + private Timestamp createTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogQueryDTO.java new file mode 100644 index 0000000..96db88c --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogQueryDTO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.biz.db.annotation.Query; + +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 日志查询类 + * @date 2020-06-01 + */ +@Data +public class LogQueryDTO { + + @Query(blurry = "username,requestIp,method,params") + private String blurry; + + @Query + private String logType; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogSmallDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogSmallDTO.java new file mode 100644 index 0000000..33fc8c6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/LogSmallDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 实体类 + * @date 2020-03-16 + */ +@Data +public class LogSmallDTO implements Serializable { + + private String description; + + private String requestIp; + + private Long time; + + private String address; + + private String browser; + + private Timestamp createTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuCreateDTO.java new file mode 100644 index 0000000..4b929c4 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuCreateDTO.java @@ -0,0 +1,94 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * @description 菜单新增 dto + * @date 2020-06-29 + */ +@Data +public class MenuCreateDTO implements Serializable { + + private static final long serialVersionUID = 3587665050240667198L; + + @NotBlank + private String name; + + private Long sort = 999L; + + @Size(max = 255, message = "路由地址长度不能超过255") + private String path; + + @Size(max = 255, message = "路径长度不能超过255") + private String component; + + /** + * 类型,目录、菜单、按钮 + */ + private Integer type; + + /** + * 权限 + */ + @Size(max = 255, message = "权限长度不能超过255") + private String permission; + + private String componentName; + + @Size(max = 255, message = "图标长度不能超过255") + private String icon; + + /** + * 布局类型 + */ + @Size(max = 255, message = "布局类型不能超过255") + private String layout; + + private Boolean cache; + + private Boolean hidden; + + /** + * 上级菜单ID + */ + private Long pid; + + + private Boolean deleted; + + /** + * 回到上一级 + */ + private String backTo; + + /** + * 扩展配置 + */ + @Size(max = 255, message = "扩展配置长度不能超过255") + private String extConfig; + + public @interface Update { + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDTO.java new file mode 100644 index 0000000..5df0a54 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDTO.java @@ -0,0 +1,82 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 字典查询转换DTO + * @date 2020-06-01 + */ +@Data +public class MenuDTO implements Serializable { + + private Long id; + + @ApiModelProperty(value = "菜单类型: 0目录,1页面,2权限,3外链") + private Integer type; + + @ApiModelProperty(value = "权限标识") + private String permission; + + @ApiModelProperty(value = "名称") + private String name; + + @ApiModelProperty(value = "菜单排序") + private Long sort; + + @ApiModelProperty(value = "路径或外链URL") + private String path; + + @ApiModelProperty(value = "组件路径") + private String component; + + @ApiModelProperty(value = "上级菜单ID") + private Long pid; + + @ApiModelProperty(value = "路由缓存 keep-alive") + private Boolean cache; + + @ApiModelProperty(value = "菜单栏不显示") + private Boolean hidden; + + @ApiModelProperty(value = "路由名称") + private String componentName; + + @ApiModelProperty(value = "菜单图标") + private String icon; + + @ApiModelProperty(value = "页面布局类型") + private String layout; + + + private List children; + + private Timestamp createTime; + + @ApiModelProperty(value = "回到上一级") + private String backTo; + + @ApiModelProperty(value = "扩展配置") + private String extConfig; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDeleteDTO.java new file mode 100644 index 0000000..91ee7b2 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuDeleteDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 菜单删除 dto + * @date 2020-06-29 + */ +@Data +public class MenuDeleteDTO implements Serializable { + + private static final long serialVersionUID = -6599428238298923816L; + + @NotEmpty(message = "id不能为空") + private Set ids; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuQueryDTO.java new file mode 100644 index 0000000..109e5bd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuQueryDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.biz.db.annotation.Query; + +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 菜单查询实体类 + * @date 2020-06-01 + */ +@Data +public class MenuQueryDTO { + + @Query(blurry = "name,path,component_name") + private String blurry; + + @Query(propName = "create_time", type = Query.Type.BETWEEN) + private List createTime; + + + @Query(propName = "deleted", type = Query.Type.EQ) + private Boolean deleted = false; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuUpdateDTO.java new file mode 100644 index 0000000..85660d9 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/MenuUpdateDTO.java @@ -0,0 +1,117 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.entity.Menu; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Objects; + +/** + * @description 菜单修改 dto + * @date 2020-06-29 + */ +@Data +public class MenuUpdateDTO implements Serializable { + + private static final long serialVersionUID = 3587665050240667198L; + + @NotNull(groups = {Menu.Update.class}) + private Long id; + + @NotBlank + private String name; + + private Long sort = 999L; + + @Size(max = 255, message = "路由地址长度不能超过255") + private String path; + + @Size(max = 255, message = "路径长度不能超过255") + private String component; + + /** + * 类型,目录、菜单、按钮 + */ + private Integer type; + + /** + * 权限 + */ + @Size(max = 255, message = "权限长度不能超过255") + private String permission; + + private String componentName; + + @Size(max = 255, message = "图标长度不能超过255") + private String icon; + + /** + * 布局类型 + */ + @Size(max = 255, message = "布局类型不能超过255") + private String layout; + + private Boolean cache; + + private Boolean hidden; + + /** + * 上级菜单ID + */ + private Long pid; + + + private Boolean deleted; + + /** + * 回到上一级 + */ + private String backTo; + + /** + * 扩展配置 + */ + @Size(max = 255, message = "扩展配置长度不能超过255") + private String extConfig; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MenuUpdateDTO menu = (MenuUpdateDTO) o; + return Objects.equals(id, menu.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public @interface Update { + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/NodeDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/NodeDTO.java new file mode 100644 index 0000000..937ca36 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/NodeDTO.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + + +/** + * @description 节点实体类 + * @date 2020-06-03 + */ +@Data +public class NodeDTO { + + + @ApiModelProperty(value = "node节点id值") + private String uid; + + @ApiModelProperty(value = "node节点名称") + private String name; + + @ApiModelProperty(value = "node节点ip地址") + private String ip; + + @ApiModelProperty(value = "node节点状态") + private String status; + + @ApiModelProperty(value = "gpu总数") + private String gpuCapacity; + + @ApiModelProperty(value = "gpu可用数") + private String gpuAvailable; + + @ApiModelProperty(value = "创建字段保存gpu使用数") + private String gpuUsed; + + @ApiModelProperty(value = "保存节点信息") + private List pods; + + @ApiModelProperty(value = "node节点的使用内存") + private String nodeMemory; + + @ApiModelProperty(value = "node节点的使用cpu") + private String nodeCpu; + + @ApiModelProperty(value = "node节点的总的cpu") + private String totalNodeCpu; + + @ApiModelProperty(value = "node节点的总的内存") + private String totalNodeMemory; + + @ApiModelProperty(value = "node节点的警告") + private String warning; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionCreateDTO.java new file mode 100644 index 0000000..82ea30b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionCreateDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.admin.domain.entity.Permission; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * @description 新增权限DTO + * @date 2021-05-31 + */ +@Data +public class PermissionCreateDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "权限父id") + @NotNull(message = "父级权限id不能为空") + private Long pid; + + @NotEmpty(message = "权限不能为空") + private List permissions; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDTO.java new file mode 100644 index 0000000..4b62b0c --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 操作权限DTO + * @date 2021-04-29 + */ +@Data +public class PermissionDTO implements Serializable { + + private static final long serialVersionUID = 1L; + @ApiModelProperty(value = "权限id") + private Long id; + + @ApiModelProperty(value = "父权限id") + private Long pid; + + @ApiModelProperty("权限标识") + private String permission; + + @ApiModelProperty(value = "权限名称") + private String name; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDeleteDTO.java new file mode 100644 index 0000000..92a6723 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionDeleteDTO.java @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Set; + +/** + * @description 删除权限DTO + * @date 2021-05-31 + */ +@Data +public class PermissionDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Set ids; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionQueryDTO.java new file mode 100644 index 0000000..77d2f35 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionQueryDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 权限查询DTO + * @date 2021-05-31 + */ +@Data +public class PermissionQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private String keyword; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionUpdateDTO.java new file mode 100644 index 0000000..2163eca --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PermissionUpdateDTO.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.admin.domain.entity.Permission; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * @description 修改权限DTO + * @date 2021-06-01 + */ +@Data +public class PermissionUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "权限id") + @NotNull(message = "权限id不能为空") + private Long id; + + @ApiModelProperty(value = "权限父id") + @NotNull(message = "父级权限id不能为空") + private Long pid; + + @NotEmpty(message = "权限不能为空") + private List permissions; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PodDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PodDTO.java new file mode 100644 index 0000000..b13c9f4 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/PodDTO.java @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * @description pod的实体类 + * @date 2020-06-03 + */ +@Data +public class PodDTO { + + @ApiModelProperty(value = "pod的name") + private String podName; + + @ApiModelProperty(value = "pod的内存") + private String podMemory; + + @ApiModelProperty(value = "pod的cpu") + private String podCpu; + + @ApiModelProperty(value = "pod的显卡") + private String podCard; + + @ApiModelProperty(value = "pod的状态") + private String status; + + @ApiModelProperty(value = "node的name") + private String nodeName; + + @ApiModelProperty(value = "pod的创建时间") + private String podCreateTime; + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsCreateDTO.java new file mode 100644 index 0000000..7c6f44e --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsCreateDTO.java @@ -0,0 +1,74 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.StringConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.*; +import java.io.Serializable; + +/** + * @description 资源规格创建 + * @date 2021-05-27 + */ +@Data +@Accessors(chain = true) +public class ResourceSpecsCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "规格名称", required = true) + @NotBlank(message = "规格名称不能为空") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "规格名称错误-输入长度不能超过32个字符") + @Pattern(regexp = StringConstant.REGEXP_SPECS, message = "规格名称支持字母、数字、汉字、英文横杠、下划线和空白字符") + private String specsName; + + @ApiModelProperty(value = "所属业务场景(0:通用,1:dubhe-notebook,2:dubhe-train,3:dubhe-serving)", required = true) + @NotNull(message = "所属业务场景不能为空") + @Min(value = MagicNumConstant.ZERO, message = "所属业务场景错误") + @Max(value = MagicNumConstant.THREE, message = "所属业务场景错误") + private Integer module; + + @ApiModelProperty(value = "CPU数量,单位:核", required = true) + @NotNull(message = "CPU数量不能为空") + @Min(value = MagicNumConstant.ZERO, message = "CPU数量不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "CPU数量超限") + private Integer cpuNum; + + @ApiModelProperty(value = "GPU数量,单位:核", required = true) + @NotNull(message = "GPU数量不能为空") + @Min(value = MagicNumConstant.ZERO, message = "GPU数量不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "GPU数量超限") + private Integer gpuNum; + + @ApiModelProperty(value = "内存大小,单位:M", required = true) + @NotNull(message = "内存数值不能为空") + @Min(value = MagicNumConstant.ZERO, message = "内存不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "内存数值超限") + private Integer memNum; + + @ApiModelProperty(value = "工作空间的存储配额,单位:M", required = true) + @NotNull(message = "工作空间的存储配额不能为空") + @Min(value = MagicNumConstant.ZERO, message = "工作空间的存储配额不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "工作空间的存储配额超限") + private Integer workspaceRequest; +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsDeleteDTO.java new file mode 100644 index 0000000..a4e4a08 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsDeleteDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 资源规格删除 + * @date 2021-05-27 + */ +@Data +@Accessors(chain = true) +public class ResourceSpecsDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为空") + private Set ids; +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsQueryDTO.java new file mode 100644 index 0000000..ff7c247 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsQueryDTO.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.db.base.PageQueryBase; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import java.io.Serializable; + +/** + * @description 查询资源规格 + * @date 2021-05-27 + */ +@Data +@Accessors(chain = true) +public class ResourceSpecsQueryDTO extends PageQueryBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("规格名称") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "规格名称错误") + private String specsName; + + @ApiModelProperty("规格类型(0为CPU, 1为GPU)") + private Boolean resourcesPoolType; + + @ApiModelProperty("所属业务场景(0:通用,1:dubhe-notebook,2:dubhe-train,3:dubhe-serving)") + @Min(value = MagicNumConstant.ZERO, message = "所属业务场景错误") + @Max(value = MagicNumConstant.THREE, message = "所属业务场景错误") + private Integer module; +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsUpdateDTO.java new file mode 100644 index 0000000..3662bb1 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/ResourceSpecsUpdateDTO.java @@ -0,0 +1,77 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.StringConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 资源规格修改 + * @date 2021-05-27 + */ +@Data +@Accessors(chain = true) +public class ResourceSpecsUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为null") + @Min(value = MagicNumConstant.ONE, message = "id必须大于1") + private Long id; + + @ApiModelProperty(value = "规格名称") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "规格名称错误-输入长度不能超过32个字符") + @Pattern(regexp = StringConstant.REGEXP_SPECS, message = "规格名称支持字母、数字、汉字、英文横杠、下划线和空白字符") + private String specsName; + + @ApiModelProperty(value = "所属业务场景(0:通用,1:dubhe-notebook,2:dubhe-train,3:dubhe-serving)", required = true) + @NotNull(message = "所属业务场景不能为空") + @Min(value = MagicNumConstant.ZERO, message = "所属业务场景错误") + @Max(value = MagicNumConstant.THREE, message = "所属业务场景错误") + private Integer module; + + @ApiModelProperty(value = "CPU数量,单位:核") + @Min(value = MagicNumConstant.ZERO, message = "CPU数量不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "CPU数量超限") + private Integer cpuNum; + + @ApiModelProperty(value = "GPU数量,单位:核") + @Min(value = MagicNumConstant.ZERO, message = "GPU数量不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "GPU数量超限") + private Integer gpuNum; + + @ApiModelProperty(value = "内存大小,单位:M") + @Min(value = MagicNumConstant.ZERO, message = "内存不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "内存数值超限") + private Integer memNum; + + @ApiModelProperty(value = "工作空间的存储配额,单位:M") + @Min(value = MagicNumConstant.ZERO, message = "工作空间的存储配额不能小于0") + @Max(value = MagicNumConstant.TWO_BILLION, message = "工作空间的存储配额超限") + private Integer workspaceRequest; +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleAuthUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleAuthUpdateDTO.java new file mode 100644 index 0000000..c47b1ce --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleAuthUpdateDTO.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description + * @date 2021-05-17 + */ +@Data +public class RoleAuthUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull(message = "角色id不能为空") + private Long roleId; + + @NotNull(message = "权限组id不能为空") + private Set authIds; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleCreateDTO.java new file mode 100644 index 0000000..81bddd1 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleCreateDTO.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.entity.Menu; + +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 角色创建 DTO + * @date 2020-06-29 + */ +@Data +public class RoleCreateDTO implements Serializable { + + private static final long serialVersionUID = -8685787591892312697L; + + private Long id; + + @Size(max = 255, message = "名称长度不能超过255") + private String name; + + /** + * 权限 + */ + @Size(max = 255, message = "默认权限长度不能超过255") + private String permission; + + @Size(max = 255, message = "备注长度不能超过255") + private String remark; + + private Set menus; + + private Boolean deleted; + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDTO.java new file mode 100644 index 0000000..23b084b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.vo.AuthVO; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.Set; + +/** + * @description 角色的实体类 + * @date 2020-06-01 + */ +@Data +public class RoleDTO implements Serializable { + + private static final long serialVersionUID = -7250301719333643312L; + private Long id; + + private String name; + + private String remark; + + private String permission; + + private Set menus; + + private Set auths; + + private Timestamp createTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDeleteDTO.java new file mode 100644 index 0000000..734b7db --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleDeleteDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 角色删除 dto + * @date 2020-06-29 + */ +@Data +public class RoleDeleteDTO implements Serializable { + + private static final long serialVersionUID = -6599428238298923816L; + + @NotEmpty(message = "角色id不能为空") + private Set ids; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleQueryDTO.java new file mode 100644 index 0000000..0986cc4 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleQueryDTO.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.biz.db.annotation.Query; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 角色请求实体DTO + * @date 2020-06-01 + */ +@Data +public class RoleQueryDTO implements Serializable { + + private static final long serialVersionUID = 1266792048261542801L; + @Query(blurry = "name,remark") + private String blurry; + + @Query(propName = "create_time", type = Query.Type.BETWEEN) + private List createTime; + + @Query(propName = "deleted", type = Query.Type.EQ) + private Boolean deleted = false; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleSmallDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleSmallDTO.java new file mode 100644 index 0000000..35642c6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleSmallDTO.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 角色的实体转换 + * @date 2020-06-01 + */ +@Data +public class RoleSmallDTO implements Serializable { + + private static final long serialVersionUID = -6601893040730122984L; + private Long id; + + private String name; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleUpdateDTO.java new file mode 100644 index 0000000..ed7402e --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/RoleUpdateDTO.java @@ -0,0 +1,75 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.entity.Menu; + +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; + +/** + * @description 角色修改 DTO + * @date 2020-06-29 + */ +@Data +public class RoleUpdateDTO implements Serializable { + + private static final long serialVersionUID = -8685787591892312697L; + + private Long id; + + @Size(max = 255, message = "名称长度不能超过255") + private String name; + + /** + * 权限 + */ + @Size(max = 255, message = "默认权限长度不能超过255") + private String permission; + + @Size(max = 255, message = "备注长度不能超过255") + private String remark; + + private Set menus; + + private Boolean deleted; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RoleUpdateDTO role = (RoleUpdateDTO) o; + return Objects.equals(id, role.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public @interface Update { + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamCreateDTO.java new file mode 100644 index 0000000..3f54027 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamCreateDTO.java @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.entity.User; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 团队创建实体 + * @date 2020-06-29 + */ +@Data +public class TeamCreateDTO implements Serializable { + + private static final long serialVersionUID = 8922409236439269071L; + + + @NotBlank + private String name; + + @NotNull + private Boolean enabled; + + /** + * 团队成员 + */ + private List teamUserList; + + private Timestamp createTime; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamQueryDTO.java new file mode 100644 index 0000000..c54f0ac --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamQueryDTO.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.biz.db.annotation.Query; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; +import java.util.Set; + +/** + * @description 团队查询DTO + * @date 2020-06-01 + */ +@Data +public class TeamQueryDTO implements Serializable { + + private static final long serialVersionUID = 700075706114767404L; + @Query(type = Query.Type.IN, propName = "id") + private Set ids; + + @Query(type = Query.Type.LIKE) + private String name; + + @Query + private Boolean enabled; + + @Query + private Long pid; + + @Query(propName = "create_time", type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUpdateDTO.java new file mode 100644 index 0000000..1a7366a --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUpdateDTO.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.entity.User; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description + * @date 2020-06-29 + */ +@Data +public class TeamUpdateDTO implements Serializable { + + private static final long serialVersionUID = 8922409236439269071L; + + @NotNull + private Long id; + + @NotBlank + private String name; + + @NotNull + private Boolean enabled; + + /** + * 团队成员 + */ + private List teamUserList; + + private Timestamp createTime; + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUserRoleSmallDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUserRoleSmallDTO.java new file mode 100644 index 0000000..7887a55 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/TeamUserRoleSmallDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 团队用户角色DTO + * @date 2020-06-01 + */ +@Data +public class TeamUserRoleSmallDTO implements Serializable { + private static final long serialVersionUID = 6589829002637730619L; + private Long id; + private RoleSmallDTO role; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserAvatarUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserAvatarUpdateDTO.java new file mode 100644 index 0000000..4044acc --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserAvatarUpdateDTO.java @@ -0,0 +1,34 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.Size; + +/** + * @description 修改头像 + * @date 2020-06-01 + */ +@Data +public class UserAvatarUpdateDTO { + + @Size(max = 255, message = "名称长度不能超过255") + private String realName; + + @Size(max = 255, message = "头像长度不能超过255") + private String path; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCenterUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCenterUpdateDTO.java new file mode 100644 index 0000000..3bd4c51 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCenterUpdateDTO.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.admin.domain.entity.User; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * @description 用户中心修改DTO + * @date 2020-06-29 + */ +@Data +public class UserCenterUpdateDTO implements Serializable { + + private static final long serialVersionUID = -6196691710092809498L; + @NotNull(groups = User.Update.class) + private Long id; + + @ApiModelProperty(value = "用户昵称") + @NotBlank(message = "用户昵称不能为空") + @Size(max = 255, message = "昵称长度不能超过255") + private String nickName; + + @ApiModelProperty(value = "性别") + private String sex; + + @NotBlank + @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式有误") + private String phone; + + private String remark; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCreateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCreateDTO.java new file mode 100644 index 0000000..d9ee743 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserCreateDTO.java @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.entity.Role; +import org.dubhe.admin.domain.entity.UserAvatar; + +import javax.validation.constraints.*; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * @description 用户创建DTO + * @date 2020-06-29 + */ +@Data +public class UserCreateDTO implements Serializable { + + private static final long serialVersionUID = -6196691710092809498L; + + + @NotBlank + @Size(max = 255, message = "名称长度不能超过255") + private String username; + + /** + * 用户昵称 + */ + @NotBlank + @Size(max = 255, message = "昵称长度不能超过255") + private String nickName; + + /** + * 性别 + */ + private String sex; + + @NotBlank + @Pattern(regexp = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$", message = "邮箱地址格式有误") + private String email; + + @NotBlank + @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式有误") + private String phone; + + @NotNull + private Boolean enabled; + + private String password; + + private Date lastPasswordResetTime; + + @Size(max = 255, message = "昵称长度不能超过255") + private String remark; + + private Long avatarId; + + + private UserAvatar userAvatar; + + + @NotEmpty + private List roles; + + private Boolean deleted; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserDeleteDTO.java new file mode 100644 index 0000000..5c6d774 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserDeleteDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 用户删除 dto + * @date 2020-06-29 + */ +@Data +public class UserDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "用户id集合") + @NotEmpty + private Set ids; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserEmailUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserEmailUpdateDTO.java new file mode 100644 index 0000000..d80f70f --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserEmailUpdateDTO.java @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 用户邮箱修改信息 + * @date 2020-06-01 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel(value = "邮箱修改请求实体", description = "邮箱修改请求实体") +public class UserEmailUpdateDTO implements Serializable { + + private static final long serialVersionUID = -5997222212073811466L; + + @NotNull(message = "用户ID不能为空") + @ApiModelProperty(value = "用户ID", name = "userId", example = "1") + private Long userId; + + @Pattern(regexp = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$", message = "邮箱地址格式有误") + @NotEmpty(message = "邮箱地址不能为空") + @ApiModelProperty(value = "邮箱地址", name = "email", example = "xx@163.com") + private String email; + + + @NotNull(message = "密码不能为空") + @ApiModelProperty(value = "密码", name = "password", example = "123456") + private String password; + + @NotNull(message = "激活码不能为空") + @ApiModelProperty(value = "激活码", name = "code", example = "998877") + private String code; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDTO.java new file mode 100644 index 0000000..00e3d8f --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDTO.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 新增用户组实体DTO + * @date 2021-05-08 + */ +@Data +public class UserGroupDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "用户组id") + private Long id; + + @ApiModelProperty(value = "用户组名称") + @NotBlank(message = "用户组名称不能为空") + @Length(max = 32, message = "名称长度不能超过32") + private String name; + + @ApiModelProperty("用户组描述") + @Length(max = 255, message = "描述长度不能超过255") + private String description; + + @ApiModelProperty(value = "用户id集合") + private Set userIds; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDeleteDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDeleteDTO.java new file mode 100644 index 0000000..b8477b6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupDeleteDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 删除用户组DTO + * @date 2021-05-11 + */ +@Data +public class UserGroupDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull(message = "用户组id不能为空") + private Set ids; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupQueryDTO.java new file mode 100644 index 0000000..768fe72 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupQueryDTO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.base.PageQueryBase; + +import java.io.Serializable; + +/** + * @description 获取用户组信息 + * @date 2021-05-06 + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode +public class UserGroupQueryDTO extends PageQueryBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "用户组id或名称") + private String keyword; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupUpdDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupUpdDTO.java new file mode 100644 index 0000000..2b10922 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserGroupUpdDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 用户组成员操作DTO + * @date 2021-05-11 + */ +@Data +public class UserGroupUpdDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "用户组id") + @NotNull(message = "用户组id不能为空") + private Long groupId; + + @ApiModelProperty(value = "用户id集合") + private Set userIds; + + @ApiModelProperty(value = "角色id集合") + private Set roleIds; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserPassUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserPassUpdateDTO.java new file mode 100644 index 0000000..387bf62 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserPassUpdateDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.Size; + +/** + * @description 修改密码的 + * @date 2020-06-01 + */ +@Data +public class UserPassUpdateDTO { + + @Size(max = 255, message = "密码长度不能超过255") + private String oldPass; + + @Size(max = 255, message = "密码长度不能超过255") + private String newPass; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserQueryDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserQueryDTO.java new file mode 100644 index 0000000..feedf31 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserQueryDTO.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.biz.db.annotation.Query; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 用户查询DTO + * @date 2020-06-01 + */ + +@Data +public class UserQueryDTO implements Serializable { + + private static final long serialVersionUID = -6863842533017963450L; + @Query + private Long id; + + @Query(blurry = "email,username,nick_name") + private String blurry; + + @Query + private Boolean enabled; + + @Query(propName = "deleted", type = Query.Type.EQ) + private Boolean deleted = false; + + private Long roleId; + + + @Query(propName = "create_time", type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterDTO.java new file mode 100644 index 0000000..afcc1f5 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterDTO.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 用户注册请求实体 + * @date 2020-06-01 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "用户注册请求实体", description = "用户注册请求实体") +public class UserRegisterDTO implements Serializable { + + private static final long serialVersionUID = -7351676930575145394L; + + @ApiModelProperty(value = "账号", name = "username", example = "test") + @NotEmpty(message = "账号不能为空") + private String username; + + @ApiModelProperty(value = "昵称", name = "nickName", example = "xt") + private String nickName; + + @ApiModelProperty(value = "性别", name = "sex", example = "1") + private Integer sex; + + @NotEmpty(message = "邮箱地址不能为空") + @Pattern(regexp = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$", message = "邮箱地址格式有误") + @ApiModelProperty(value = "邮箱地址", name = "email", example = "xxx@163.com") + private String email; + + @NotNull(message = "手机号不能为空") + @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式有误") + @ApiModelProperty(value = "手机号", name = "phone", example = "13823370116") + private String phone; + + + @NotNull(message = "密码不能为空") + @ApiModelProperty(value = "密码", name = "password", example = "123456") + private String password; + + @NotNull(message = "激活码不能为空") + @ApiModelProperty(value = "激活码", name = "code", example = "998877") + private String code; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterMailDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterMailDTO.java new file mode 100644 index 0000000..739460c --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRegisterMailDTO.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 用户注册邮箱请求实体 + * @date 2020-06-01 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "发送邮箱验证码请求实体", description = "发送邮箱验证码请求实体") +public class UserRegisterMailDTO implements Serializable { + private static final long serialVersionUID = 5063855150803214253L; + + @NotNull(message = "类型不能为空") + @ApiModelProperty(value = "类型 1 用户注册 2 修改邮箱 3 其他 4 忘记密码", name = "type", example = "1") + private Integer type; + + @NotEmpty(message = "邮箱地址不能为空") + @Pattern(regexp = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$", message = "邮箱地址格式有误") + @ApiModelProperty(value = "邮箱地址", name = "email", example = "xxx@163.com") + private String email; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserResetPasswordDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserResetPasswordDTO.java new file mode 100644 index 0000000..9453926 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserResetPasswordDTO.java @@ -0,0 +1,57 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 用户重置密码请求实体 + * @date 2020-06-01 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "用户重置密码请求实体", description = "用户重置密码请求实体") +public class UserResetPasswordDTO implements Serializable { + + + private static final long serialVersionUID = -4249894291904235207L; + + @NotEmpty(message = "邮箱地址不能为空") + @Pattern(regexp = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$", message = "邮箱地址格式有误") + @ApiModelProperty(value = "邮箱地址", name = "email", example = "xxx@163.com") + private String email; + + @NotNull(message = "密码不能为空") + @ApiModelProperty(value = "密码", name = "password", example = "123456") + private String password; + + @NotNull(message = "激活码不能为空") + @ApiModelProperty(value = "激活码", name = "code", example = "998877") + private String code; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRoleUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRoleUpdateDTO.java new file mode 100644 index 0000000..b3cd28d --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserRoleUpdateDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 批量修改用户组用户的角色 + * @date 2021-05-12 + */ +@Data +public class UserRoleUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "用户组id") + @NotNull(message = "用户组id不能为空") + private Long groupId; + + @ApiModelProperty("角色id集合") + @NotNull(message = "角色id不能为空") + private Set roleIds; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserStateUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserStateUpdateDTO.java new file mode 100644 index 0000000..c8d59c4 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserStateUpdateDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 批量修改用户组用户状态DTO + * @date 2021-05-12 + */ +@Data +public class UserStateUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value ="用户组id" ) + @NotNull(message = "用户组id不能为空") + private Long groupId; + + @ApiModelProperty("用户状态:激活:true,锁定:false") + private boolean enabled; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserUpdateDTO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserUpdateDTO.java new file mode 100644 index 0000000..9215ca8 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/dto/UserUpdateDTO.java @@ -0,0 +1,86 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.dto; + +import lombok.Data; +import org.dubhe.admin.domain.entity.Role; +import org.dubhe.admin.domain.entity.User; +import org.dubhe.admin.domain.entity.UserAvatar; + +import javax.validation.constraints.*; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * @description 用户修改DTO + * @date 2020-06-29 + */ +@Data +public class UserUpdateDTO implements Serializable { + + private static final long serialVersionUID = -6196691710092809498L; + @NotNull(groups = User.Update.class) + private Long id; + + @NotBlank + @Size(max = 255, message = "名称长度不能超过255") + private String username; + + /** + * 用户昵称 + */ + @NotBlank + @Size(max = 255, message = "昵称长度不能超过255") + private String nickName; + + /** + * 性别 + */ + private String sex; + + @NotBlank + @Pattern(regexp = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$", message = "邮箱地址格式有误") + private String email; + + @NotBlank + @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式有误") + private String phone; + + @NotNull + private Boolean enabled; + + private String password; + + private Date lastPasswordResetTime; + + @Size(max = 255, message = "昵称长度不能超过255") + private String remark; + + private Long avatarId; + + + private UserAvatar userAvatar; + + + @NotEmpty + private List roles; + + private Boolean deleted; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Auth.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Auth.java new file mode 100644 index 0000000..1570deb --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Auth.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import org.dubhe.biz.db.entity.BaseEntity; + +/** + * @description 权限组实体类 + * @date 2021-05-14 + */ +@Data +@TableName("auth") +public class Auth extends BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "auth_code") + private String authCode; + + @TableField(value = "description") + private String description; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/AuthPermission.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/AuthPermission.java new file mode 100644 index 0000000..d74dd3a --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/AuthPermission.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; + +/** + * @description + * @date 2021-05-14 + */ +@Data +@TableName("auth_permission") +public class AuthPermission implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 权限组id + */ + @TableField(value = "auth_id") + private Long authId; + + /** + * 权限id + */ + @TableField(value = "permission_id") + private Long permissionId; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DataSequence.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DataSequence.java new file mode 100644 index 0000000..b1eddb3 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DataSequence.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @description 序列表 + * @date 2020-09-23 + */ +@Data +@TableName("data_sequence") +public class DataSequence { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "business_code") + private String businessCode; + + @TableField(value = "start") + private Long start; + + @TableField(value = "step") + private Long step; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Dict.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Dict.java new file mode 100644 index 0000000..14824fd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Dict.java @@ -0,0 +1,75 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 字典实体 + * @date 2020-06-01 + */ +@Data +@TableName("dict") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Dict implements Serializable { + + private static final long serialVersionUID = -3995510721958462699L; + @TableId(value = "id", type = IdType.AUTO) + @NotNull(groups = Update.class) + private Long id; + + /** + * 名称 + */ + @TableField(value = "name") + @NotBlank + @Length(max = 255, message = "名称长度不能超过255") + private String name; + + /** + * 备注 + */ + @TableField(value = "remark") + @Length(max = 255, message = "备注长度不能超过255") + private String remark; + + + @TableField(value = "create_time") + private Timestamp createTime; + + @TableField(exist = false) + private List dictDetails; + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DictDetail.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DictDetail.java new file mode 100644 index 0000000..cd3dc60 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/DictDetail.java @@ -0,0 +1,77 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 字典详情实体 + * @date 2020-06-01 + */ +@Data +@TableName("dict_detail") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DictDetail implements Serializable { + + private static final long serialVersionUID = 299242717738411209L; + @TableId(value = "id", type = IdType.AUTO) + @NotNull(groups = Update.class) + private Long id; + + /** + * 字典标签 + */ + @TableField(value = "label") + @Length(max = 255, message = "字典标签长度不能超过255") + private String label; + + /** + * 字典值 + */ + @TableField(value = "value") + @Length(max = 255, message = "字典值长度不能超过255") + private String value; + + /** + * 排序 + */ + @TableField(value = "sort") + private String sort = "999"; + + @TableField(value = "dict_id") + private Long dictId; + + @TableField(value = "create_time") + private Timestamp createTime; + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Group.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Group.java new file mode 100644 index 0000000..99e18c0 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Group.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.db.entity.BaseEntity; + +/** + * @description 用户组实体类 + * @date 2021-05-06 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("pt_group") +public class Group extends BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "name") + private String name; + + @TableField(value = "description") + private String description; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Log.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Log.java new file mode 100644 index 0000000..50eca54 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Log.java @@ -0,0 +1,106 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.domain.entity; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 日志实体 + * @date 2020-06-01 + */ +@Data +@TableName("log") +@NoArgsConstructor +public class Log implements Serializable { + + private static final long serialVersionUID = -4447644691937249474L; + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 操作用户 + */ + @TableField(value = "username") + private String username; + + /** + * 描述 + */ + @TableField(value = "description") + private String description; + + /** + * 方法名 + */ + @TableField(value = "label") + private String method; + + /** + * 参数 + */ + @TableField(value = "text") + private String params; + + /** + * 日志类型 + */ + @TableField(value = "log_type") + private String logType; + + /** + * 请求ip + */ + @TableField(value = "request_ip") + private String requestIp; + + /** + * 浏览器 + */ + @TableField(value = "browser") + private String browser; + + /** + * 请求耗时 + */ + @TableField(value = "time") + private Long time; + + /** + * 异常详细 + */ + @TableField(value = "exception_detail") + private byte[] exceptionDetail; + + /** + * 创建日期 + */ + @TableField(value = "create_time") + private Timestamp createTime; + + public Log(String logType, Long time) { + this.logType = logType; + this.time = time; + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Menu.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Menu.java new file mode 100644 index 0000000..0866505 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Menu.java @@ -0,0 +1,109 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.db.entity.BaseEntity; + +import java.io.Serializable; + +/** + * @description 菜单实体 + * @date 2020-11-30 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName("menu") +public class Menu extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 3100515433018008777L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String name; + + @TableField(value = "sort") + private Long sort = 999L; + + @TableField(value = "path") + private String path; + + @TableField(value = "component") + private String component; + + /** + * 类型,目录、菜单、按钮 + */ + @TableField(value = "type") + private Integer type; + + /** + * 权限 + */ + @TableField(value = "permission") + private String permission; + + @TableField(value = "component_name") + private String componentName; + + @TableField(value = "icon") + private String icon; + + /** + * 布局类型 + */ + @TableField(value = "layout") + private String layout; + + @TableField(value = "cache") + private Boolean cache; + + @TableField(value = "hidden") + private Boolean hidden; + + /** + * 上级菜单ID + */ + @TableField(value = "pid") + private Long pid; + + @TableField(value = "deleted",fill = FieldFill.INSERT) + private Boolean deleted; + + /** + * 回到上一级 + */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + private String backTo; + + /** + * 扩展配置 + */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + private String extConfig; + + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Permission.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Permission.java new file mode 100644 index 0000000..6c55db9 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Permission.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; + +/** + * @description + * @date 2021-04-26 + */ +@Data +@TableName("permission") +@Accessors(chain = true) +public class Permission extends BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableId(value = "pid") + private Long pid; + + @TableField(value = "name") + private String name; + + @TableField(value = "permission") + private String permission; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/ResourceSpecs.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/ResourceSpecs.java new file mode 100644 index 0000000..a3f7307 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/ResourceSpecs.java @@ -0,0 +1,87 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; + +import javax.validation.constraints.NotNull; + +/** + * @description 资源规格实体类 + * @date 2021-05-27 + */ +@Data +@Accessors(chain = true) +@TableName("resource_specs") +public class ResourceSpecs extends BaseEntity { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + @NotNull(groups = {Update.class}) + private Long id; + + /** + * 规格名称 + */ + @TableField(value = "specs_name") + private String specsName; + + /** + * 规格类型(0为CPU, 1为GPU) + */ + @TableField(value = "resources_pool_type") + private Boolean resourcesPoolType; + + /** + * 所属业务场景 + */ + @TableField(value = "module") + private Integer module; + + /** + * CPU数量,单位:m(毫核) + */ + @TableField(value = "cpu_num") + private Integer cpuNum; + + /** + * GPU数量,单位:核 + */ + @TableField(value = "gpu_num") + private Integer gpuNum; + + /** + * 内存大小,单位:Mi + */ + @TableField(value = "mem_num") + private Integer memNum; + + /** + * 工作空间的存储配额,单位:Mi + */ + @TableField(value = "workspace_request") + private Integer workspaceRequest; + +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Role.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Role.java new file mode 100644 index 0000000..9c0f152 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Role.java @@ -0,0 +1,69 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.db.entity.BaseEntity; + +import java.io.Serializable; +import java.util.Set; + +/** + * @description 角色实体 + * @date 2020-11-29 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName("role") +public class Role extends BaseEntity implements Serializable { + + private static final long serialVersionUID = -812009584744832371L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "name") + private String name; + + /** + * 权限 + */ + @TableField(value = "permission") + private String permission; + + @TableField(value = "remark") + private String remark; + + @TableField(exist = false) + private Set menus; + + @TableField(exist = false) + private Set auths; + + @TableField(value = "deleted", fill = FieldFill.INSERT) + private Boolean deleted = false; + + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleAuth.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleAuth.java new file mode 100644 index 0000000..e5de66e --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleAuth.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; + +/** + * @description + * @date 2021-05-17 + */ +@Data +@TableName("roles_auth") +public class RoleAuth implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableField(value = "role_id") + private Long roleId; + + @TableField(value = "auth_id") + private Long authId; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleMenu.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleMenu.java new file mode 100644 index 0000000..6c0fa74 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/RoleMenu.java @@ -0,0 +1,49 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + + +/** + * @description 角色菜单关系实体 + * @date 2020-06-29 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName("roles_menus") +public class RoleMenu implements Serializable { + + private static final long serialVersionUID = -6296866205797727963L; + + @TableField(value = "menu_id") + private Long menuId; + + @TableField(value = "role_id") + private Long roleId; + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Team.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Team.java new file mode 100644 index 0000000..760fa47 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/Team.java @@ -0,0 +1,68 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 团队实体 + * @date 2020-06-29 + */ +@TableName("team") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Team { + @TableId(value = "id", type = IdType.AUTO) + @NotNull(groups = Update.class) + private Long id; + + @TableField(value = "name") + @NotBlank + private String name; + + @TableField(value = "enabled") + @NotNull + private Boolean enabled; + + /** + * 团队成员 + */ + @TableField(exist = false) + private List teamUserList; + + @TableField(value = "create_time") + private Timestamp createTime; + + + public @interface Update { + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/TeamUserRole.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/TeamUserRole.java new file mode 100644 index 0000000..d3b59ec --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/TeamUserRole.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 团队用户关系实体 + * @date 2020-06-29 + */ +@Data +@TableName("teams_users_roles") +public class TeamUserRole implements Serializable { + + @TableId(value = "id", type = IdType.AUTO) + @NotNull() + private Long id; + + /** + * 团队 + */ + @TableField(exist = false) + private Team team; + + /** + * 用户 + */ + @TableField(exist = false) + private User user; + + /** + * 角色 + */ + @TableField(exist = false) + private Role role; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/User.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/User.java new file mode 100644 index 0000000..ed413bd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/User.java @@ -0,0 +1,94 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.db.entity.BaseEntity; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + + +/** + * @description 用户实体 + * @date 2020-11-29 + */ +@Data +@TableName("user") +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class User extends BaseEntity implements Serializable { + + private static final long serialVersionUID = -3836401769559845765L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "username") + private String username; + + /** + * 用户昵称 + */ + @TableField(value = "nick_name") + private String nickName; + + /** + * 性别 + */ + @TableField(value = "sex") + private String sex; + + @TableField(value = "email") + private String email; + + @TableField(value = "phone") + private String phone; + + @TableField(value = "enabled") + private Boolean enabled; + + @TableField(value = "password") + private String password; + + @TableField(value = "last_password_reset_time") + private Date lastPasswordResetTime; + + @TableField(value = "remark") + private String remark; + + @TableField(value = "avatar_id") + private Long avatarId; + + @TableField(value = "deleted",fill = FieldFill.INSERT) + private Boolean deleted = false; + + @TableField(exist = false) + private UserAvatar userAvatar; + + + @NotEmpty + @TableField(exist = false) + private List roles; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserAvatar.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserAvatar.java new file mode 100644 index 0000000..7da9d67 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserAvatar.java @@ -0,0 +1,58 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.domain.entity; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.db.entity.BaseEntity; + +import java.io.Serializable; + + +/** + * @description 用户头像实体 + * @date 2020-06-29 + */ +@Data +@NoArgsConstructor +@TableName("user_avatar") +public class UserAvatar extends BaseEntity implements Serializable { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "real_name") + private String realName; + + @TableField(value = "path") + private String path; + + @TableField(value = "size") + private String size; + + public UserAvatar(UserAvatar userAvatar, String realName, String path, String size) { + this.id = ObjectUtil.isNotEmpty(userAvatar) ? userAvatar.getId() : null; + this.realName = realName; + this.path = path; + this.size = size; + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserRole.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserRole.java new file mode 100644 index 0000000..2e1f2eb --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/entity/UserRole.java @@ -0,0 +1,47 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @description 用户角色关系实体 + * @date 2020-11-29 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName("users_roles") +public class UserRole implements Serializable { + + private static final long serialVersionUID = -6296866205797727963L; + + @TableField(value = "user_id") + private Long userId; + + @TableField(value = "role_id") + private Long roleId; + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/AuthVO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/AuthVO.java new file mode 100644 index 0000000..669896c --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/AuthVO.java @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.admin.domain.entity.Permission; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description + * @date 2021-05-17 + */ +@Data +public class AuthVO implements Serializable { + + private static final long serialVersionUID = -1584916470133872539L; + + @ApiModelProperty("权限组id") + private Long id; + + @ApiModelProperty("权限组名称") + private String authCode; + + @ApiModelProperty("创建人") + private Long createUserId; + + @ApiModelProperty("修改人") + private Long updateUserId; + + @ApiModelProperty("创建时间") + private Timestamp createTime; + + @ApiModelProperty("修改时间") + private Timestamp updateTime; + + @ApiModelProperty("描述") + private String description; + + @ApiModelProperty("权限列表") + private List permissions; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/EmailVo.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/EmailVo.java new file mode 100644 index 0000000..e5fb0f2 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/EmailVo.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @description 用户邮箱信息 + * @date 2020-06-01 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EmailVo implements Serializable { + private static final long serialVersionUID = -5997222212073811466L; + + private String email; + + private String code; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuMetaVo.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuMetaVo.java new file mode 100644 index 0000000..f6b96a6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuMetaVo.java @@ -0,0 +1,39 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 菜单元数据 + * @date 2020-06-01 + */ +@Data +@AllArgsConstructor +public class MenuMetaVo implements Serializable { + + private static final long serialVersionUID = 4641840267582701511L; + private String title; + + private String icon; + + private String layout; + + private Boolean noCache; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuVo.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuVo.java new file mode 100644 index 0000000..736b0d6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/MenuVo.java @@ -0,0 +1,44 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.domain.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 菜单VO + * @date 2020-06-01 + */ +@Data +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class MenuVo implements Serializable { + + private static final long serialVersionUID = 7145999097655311261L; + private String name; + + private String path; + + private Boolean hidden; + + private String component; + + private MenuMetaVo meta; + + private List children; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/PermissionVO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/PermissionVO.java new file mode 100644 index 0000000..e87f44b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/PermissionVO.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 权限VO + * @date 2021-05-31 + */ +@Data +public class PermissionVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private Long pid; + + private String permission; + + private String name; + + private Timestamp createTime; + + private Timestamp updateTime; + + private List children; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/ResourceSpecsQueryVO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/ResourceSpecsQueryVO.java new file mode 100644 index 0000000..9099476 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/ResourceSpecsQueryVO.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 资源规格查询结果封装类 + * @date 2021-05-27 + */ +@Data +@Accessors(chain = true) +public class ResourceSpecsQueryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("主键ID") + private Long id; + + @ApiModelProperty("规格名称") + private String specsName; + + @ApiModelProperty("规格类型(0为CPU, 1为GPU)") + private Boolean resourcesPoolType; + + @ApiModelProperty("所属业务场景") + private Integer module; + + @ApiModelProperty("CPU数量,单位:核") + private Integer cpuNum; + + @ApiModelProperty("GPU数量,单位:核") + private Integer gpuNum; + + @ApiModelProperty("内存大小,单位:Mi") + private Integer memNum; + + @ApiModelProperty("工作空间的存储配额,单位:Mi") + private Integer workspaceRequest; + + @ApiModelProperty("创建人") + private Long createUserId; + + @ApiModelProperty("创建时间") + private Timestamp createTime; + + @ApiModelProperty("更新人") + private Long updateUserId; + + @ApiModelProperty("更新时间") + private Timestamp updateTime; +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserGroupVO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserGroupVO.java new file mode 100644 index 0000000..6f5caf2 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserGroupVO.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 用户组信息 + * @date 2021-05-11 + */ +@Data +public class UserGroupVO implements Serializable { + + private static final long serialVersionUID = -1584916470133872539L; + + @ApiModelProperty("用户组Id") + private Long id; + + @ApiModelProperty("用户组名称") + private String name; + + @ApiModelProperty("创建人") + private Long createUserId; + + @ApiModelProperty("修改人") + private Long updateUserId; + + @ApiModelProperty("创建时间") + private Timestamp createTime; + + @ApiModelProperty("修改时间") + private Timestamp updateTime; + + @ApiModelProperty("描述") + private String description; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserVO.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserVO.java new file mode 100644 index 0000000..b2fe687 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/domain/vo/UserVO.java @@ -0,0 +1,58 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dubhe.admin.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @description 用户VO + * @date 2020-06-29 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UserVO implements Serializable { + + private static final long serialVersionUID = -8697100416579599857L; + + /** + * 账号 + */ + private String username; + + /** + * 邮箱地址 + */ + private String email; + + /** + * 密码 : 账号 + md5 + */ + private String password; + + /** + * 是否有管理员权限 + */ + private Boolean is_staff; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/MenuTypeEnum.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/MenuTypeEnum.java new file mode 100644 index 0000000..df761f6 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/MenuTypeEnum.java @@ -0,0 +1,101 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.enums; + +/** + * @description 菜单类型 + * @date 2020-06-01 + */ +public enum MenuTypeEnum { + + /** + * DIR_TYPE 目录 + */ + DIR_TYPE(0, "目录"), + + /** + * PAGE_TYPE 页面 + */ + PAGE_TYPE(1, "页面"), + + /** + * ACTION_TYPE 操作(权限) + */ + ACTION_TYPE(2, "操作"), + + /** + * LINK_TYPE 外链 + */ + LINK_TYPE(3, "外链"), + + /** + * OTHER_TYPE 其他 + */ + OTHER_TYPE(-1, "其他"), + + ; + + private Integer value; + + private String desc; + + MenuTypeEnum(Integer value, String desc) { + this.value = value; + this.desc = desc; + } + + public Integer getValue() { + return this.value; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public static MenuTypeEnum getEnumValue(Integer value) { + switch (value) { + case 0: + return DIR_TYPE; + case 1: + return PAGE_TYPE; + case 2: + return ACTION_TYPE; + case 3: + return LINK_TYPE; + default: + return OTHER_TYPE; + } + } + + public static boolean isExist(Integer value) { + for (MenuTypeEnum itm : MenuTypeEnum.values()) { + if (value.compareTo(itm.getValue())==0) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "[" + this.value + "]" + this.desc; + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/SystemNodeEnum.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/SystemNodeEnum.java new file mode 100644 index 0000000..55d7b81 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/SystemNodeEnum.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.enums; + +/** + * @description 节点枚举类 + * @date 2020-07-08 + */ +public enum SystemNodeEnum { + /** + * 网络资源 + */ + NETWORK("NetworkUnavailable", "网络资源不足"), + /** + * 内存资源 + */ + MEMORY("MemoryPressure", "内存资源不足"), + /** + * 磁盘资源 + */ + DISK("DiskPressure", "磁盘资源不足"), + /** + * 进程资源 + */ + PROCESS("PIDPressure", "进程资源不足"); + + private String type; + private String message; + + SystemNodeEnum(String type, String message) { + this.type = type; + this.message = message; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + /** + * + * 根据type查询message信息 + * @param systemNodeType 节点类型 + * @return String message信息 + */ + public static String findMessageByType(String systemNodeType) { + SystemNodeEnum[] systemNodeEnums = SystemNodeEnum.values(); + for (SystemNodeEnum systemNodeEnum : systemNodeEnums) { + if (systemNodeType.equals(systemNodeEnum.getType())) { + return systemNodeEnum.getMessage(); + } + } + return null; + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/UserMailCodeEnum.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/UserMailCodeEnum.java new file mode 100644 index 0000000..8239814 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/enums/UserMailCodeEnum.java @@ -0,0 +1,96 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.enums; + +/** + * @description 邮箱code 类型 + * @date 2020-06-01 + */ +public enum UserMailCodeEnum { + /** + * REGISTER_CODE 天枢:注册激活验证 + */ + REGISTER_CODE(1, "天枢:注册激活验证"), + + /** + * MAIL_UPDATE_CODE 天枢:邮箱修改验证 + */ + MAIL_UPDATE_CODE(2, "天枢:邮箱修改验证"), + + /** + * OTHER_CODE 天枢:其他验证码 + */ + OTHER_CODE(3, "天枢:其他验证码"), + + /** + * FORGET_PASSWORD 天枢:忘记密码验证 + */ + FORGET_PASSWORD(4, "天枢:忘记密码验证"), + + ; + + private Integer value; + + private String desc; + + UserMailCodeEnum(Integer value, String desc) { + this.value = value; + this.desc = desc; + } + + public Integer getValue() { + return this.value; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public static UserMailCodeEnum getEnumValue(Integer value) { + switch (value) { + case 1: + return REGISTER_CODE; + case 2: + return MAIL_UPDATE_CODE; + case 4: + return FORGET_PASSWORD; + default: + return OTHER_CODE; + } + } + + public static boolean isExist(Integer value) { + for (UserMailCodeEnum itm : UserMailCodeEnum.values()) { + if (value.compareTo(itm.getValue()) == 0) { + return true; + } + } + return false; + } + + + @Override + public String toString() { + return "[" + this.value + "]" + this.desc; + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/event/BaseEvent.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/BaseEvent.java new file mode 100644 index 0000000..9901d35 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/BaseEvent.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.event; + +import org.springframework.context.ApplicationEvent; + + +/** + * @description 超类 事件 + * @date 2020-06-01 + */ +public abstract class BaseEvent extends ApplicationEvent { + + private static final long serialVersionUID = 895628808370649881L; + + protected T eventData; + + public BaseEvent(Object source, T eventData) { + super(source); + this.eventData = eventData; + } + + public BaseEvent(T eventData) { + super(eventData); + } + + public T getEventData() { + return eventData; + } + + public void setEventData(T eventData) { + this.eventData = eventData; + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEvent.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEvent.java new file mode 100644 index 0000000..6ef4334 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEvent.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.event; + +import org.dubhe.admin.domain.dto.EmailDTO; + + +/** + * @description 邮件事件 + * @date 2020-06-01 + */ +public class EmailEvent extends BaseEvent { + + private static final long serialVersionUID = 8103187726344703089L; + + public EmailEvent(EmailDTO msg) { + super(msg); + } + + public EmailEvent(Object source, EmailDTO msg) { + super(source, msg); + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventListener.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventListener.java new file mode 100644 index 0000000..e206e77 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventListener.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.event; + +import org.dubhe.admin.domain.dto.EmailDTO; +import org.dubhe.admin.service.MailService; +import org.dubhe.biz.base.enums.BaseErrorCodeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + + +/** + * @description 邮箱事件监听 + * @date 2020-06-01 + */ +@Component +public class EmailEventListener { + + @Resource + private MailService mailService; + + + @EventListener + @Async("taskExecutor") + public void onApplicationEvent(EmailEvent event) { + EmailDTO emailDTO = (EmailDTO) event.getSource(); + sendMail(emailDTO.getReceiverMailAddress(), emailDTO.getSubject(), emailDTO.getCode()); + } + + + /** + * 发送邮件 + * + * @param receiverMailAddress 接受邮箱地址 + * @param subject 标题 + * @param code 验证码 + */ + public void sendMail(final String receiverMailAddress, String subject, String code) { + try { + final StringBuffer sb = new StringBuffer(); + sb.append("

" + "亲爱的" + receiverMailAddress + "您好!

") + .append("

您的验证码为:" + code + "

"); + mailService.sendHtmlMail(receiverMailAddress, subject, sb.toString()); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl sendMail error , param:{} error:{}", receiverMailAddress, e); + throw new BusinessException(BaseErrorCodeEnum.ERROR_SYSTEM.getCode(), BaseErrorCodeEnum.ERROR_SYSTEM.getMsg()); + } + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventPublisher.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventPublisher.java new file mode 100644 index 0000000..9e69028 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/event/EmailEventPublisher.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.event; + +import org.dubhe.admin.domain.dto.EmailDTO; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @description 邮箱事件发布者 + * @date 2020-06-01 + */ +@Service +public class EmailEventPublisher { + + @Resource + private ApplicationEventPublisher applicationEventPublisher; + + /** + * 邮件发送事件 + * + * @param dto + */ + @Async("taskExecutor") + public void sentEmailEvent(final EmailDTO dto) { + try { + EmailEvent emailEvent = new EmailEvent(dto); + applicationEventPublisher.publishEvent(emailEvent); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "EmailEventPublisher sentEmailEvent error , param:{} error:{}", dto, e); + } + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/AuthCodeController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/AuthCodeController.java new file mode 100644 index 0000000..f69430e --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/AuthCodeController.java @@ -0,0 +1,88 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.AuthCodeCreateDTO; +import org.dubhe.admin.domain.dto.AuthCodeDeleteDTO; +import org.dubhe.admin.domain.dto.AuthCodeQueryDTO; +import org.dubhe.admin.domain.dto.AuthCodeUpdateDTO; +import org.dubhe.admin.service.AuthCodeService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @description + * @date 2021-05-17 + */ +@Api(tags = "系统:权限管理") +@RestController +@RequestMapping("/authCode") +public class AuthCodeController { + + @Autowired + private AuthCodeService authCodeService; + + @GetMapping + @ApiOperation("获取权限组列表") + @PreAuthorize(Permissions.AUTH_CODE) + public DataResponseBody queryAll(AuthCodeQueryDTO authCodeQueryDTO) { + return new DataResponseBody(authCodeService.queryAll(authCodeQueryDTO)); + } + + @PostMapping + @ApiOperation("创建权限组") + @PreAuthorize(Permissions.AUTH_CODE_CREATE) + public DataResponseBody create(@Validated @RequestBody AuthCodeCreateDTO authCodeCreateDTO) { + authCodeService.create(authCodeCreateDTO); + return new DataResponseBody(); + } + + @PutMapping() + @ApiOperation("修改权限组") + @PreAuthorize(Permissions.AUTH_CODE_EDIT) + public DataResponseBody update(@Validated @RequestBody AuthCodeUpdateDTO authCodeUpdateDTO) { + authCodeService.update(authCodeUpdateDTO); + return new DataResponseBody(); + } + + @DeleteMapping + @ApiOperation("删除权限组") + @PreAuthorize(Permissions.AUTH_CODE_DELETE) + public DataResponseBody delete(@RequestBody AuthCodeDeleteDTO authCodeDeleteDTO) { + authCodeService.delete(authCodeDeleteDTO.getIds()); + return new DataResponseBody(); + } + + @GetMapping("list") + @ApiOperation("获取权限组tree") + @PreAuthorize(Permissions.AUTH_CODE) + public DataResponseBody getAuthCodeList() { + return new DataResponseBody(authCodeService.getAuthCodeList()); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictController.java new file mode 100644 index 0000000..d4c351b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictController.java @@ -0,0 +1,108 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.DictCreateDTO; +import org.dubhe.admin.domain.dto.DictDeleteDTO; +import org.dubhe.admin.domain.dto.DictQueryDTO; +import org.dubhe.admin.domain.dto.DictUpdateDTO; +import org.dubhe.admin.service.DictService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; + +/** + * @description 字典管理 控制器 + * @date 2020-06-01 + */ +@Api(tags = "系统:字典管理") +@RestController +@RequestMapping("/dict") +public class DictController { + + @Autowired + private DictService dictService; + + + @ApiOperation("导出字典数据") + @GetMapping(value = "/download") + @PreAuthorize(Permissions.DICT_DOWNLOAD) + public void download(HttpServletResponse response, DictQueryDTO criteria) throws IOException { + dictService.download(dictService.queryAll(criteria), response); + } + + @ApiOperation("查询字典") + @GetMapping(value = "/all") + @PreAuthorize(Permissions.DICT) + public DataResponseBody all() { + return new DataResponseBody(dictService.queryAll(new DictQueryDTO())); + } + + @ApiOperation("查询字典") + @GetMapping + @PreAuthorize(Permissions.DICT) + public DataResponseBody getDicts(DictQueryDTO resources, Page page) { + return new DataResponseBody(dictService.queryAll(resources, page)); + } + + @ApiOperation("新增字典") + @PostMapping + @PreAuthorize(Permissions.DICT_CREATE) + public DataResponseBody create(@Valid @RequestBody DictCreateDTO resources) { + return new DataResponseBody(dictService.create(resources)); + } + + @ApiOperation("修改字典") + @PutMapping + @PreAuthorize(Permissions.DICT_EDIT) + public DataResponseBody update(@Valid @RequestBody DictUpdateDTO resources) { + dictService.update(resources); + return new DataResponseBody(); + } + + @ApiOperation("批量删除字典") + @DeleteMapping + @PreAuthorize(Permissions.DICT_DELETE) + public DataResponseBody delete(@RequestBody DictDeleteDTO dto) { + dictService.deleteAll(dto.getIds()); + return new DataResponseBody(); + } + + @ApiOperation("根据名称查询字典详情") + @GetMapping(value = "/{name}") + @PreAuthorize(Permissions.DICT) + public DataResponseBody getDict(@PathVariable String name) { + return DataResponseFactory.success(dictService.findByName(name)); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictDetailController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictDetailController.java new file mode 100644 index 0000000..54a0a32 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/DictDetailController.java @@ -0,0 +1,96 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.DictDetailCreateDTO; +import org.dubhe.admin.domain.dto.DictDetailDeleteDTO; +import org.dubhe.admin.domain.dto.DictDetailQueryDTO; +import org.dubhe.admin.domain.dto.DictDetailUpdateDTO; +import org.dubhe.admin.service.DictDetailService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.dto.DictDetailQueryByLabelNameDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.base.vo.DictDetailVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +/** + * @description 字典详情管理 控制器 + * @date 2020-06-01 + */ +@Api(tags = "系统:字典详情管理") +@RestController +@RequestMapping("/dictDetail") +public class DictDetailController { + + @Autowired + private DictDetailService dictDetailService; + + @ApiOperation("查询字典详情") + @GetMapping + @PreAuthorize(Permissions.DICT) + public DataResponseBody getDictDetails(DictDetailQueryDTO resources, Page page) { + return new DataResponseBody(dictDetailService.queryAll(resources, page)); + } + + + @ApiOperation("新增字典详情") + @PostMapping + @PreAuthorize(Permissions.DICT_DETAIL_CREATE) + public DataResponseBody create(@Valid @RequestBody DictDetailCreateDTO resources) { + return new DataResponseBody(dictDetailService.create(resources)); + } + + + @ApiOperation("修改字典详情") + @PutMapping + @PreAuthorize(Permissions.DICT_DETAIL_EDIT) + public DataResponseBody update(@Valid @RequestBody DictDetailUpdateDTO resources) { + dictDetailService.update(resources); + return new DataResponseBody(); + } + + @ApiOperation("删除字典详情") + @DeleteMapping + @PreAuthorize(Permissions.DICT_DETAIL_DELETE) + public DataResponseBody delete(@Valid @RequestBody DictDetailDeleteDTO dto) { + dictDetailService.delete(dto.getIds()); + return new DataResponseBody(); + } + + @ApiOperation("根据名称查询字典详情") + @GetMapping("/getDictDetails") + @PreAuthorize(Permissions.DICT) + public DataResponseBody> findDictDetailByName(@Validated DictDetailQueryByLabelNameDTO dictDetailQueryByLabelNameDTO) { + return new DataResponseBody(dictDetailService.getDictName(dictDetailQueryByLabelNameDTO)); + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ForwardController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ForwardController.java new file mode 100644 index 0000000..d826675 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ForwardController.java @@ -0,0 +1,126 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.rest; + +import cn.hutool.core.util.StrUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * @description 代理转发 + * @date 2020-06-23 + */ +@RestController +public class ForwardController { + @Value("${dubhe-proxy.visual.keyword}") + private String visual; + @Value("${dubhe-proxy.visual.server}") + private String visualServer; + @Value("${dubhe-proxy.visual.port}") + private String visualPort; + @Value("${dubhe-proxy.refine.keyword}") + private String refine; + @Value("${dubhe-proxy.refine.server}") + private String refineServer; + @Value("${dubhe-proxy.refine.port}") + private String refinePort; + + RestTemplate restTemplate = new RestTemplate(); + + /** + * 根据不同的请求拼上对应的转发路径 + * + * @param request http请求 + * @return URI 用于restTemplate的请求路径 + **/ + private URI getURI(HttpServletRequest request) throws URISyntaxException { + String requestURI = request.getRequestURI(); + String server = null; + String prefix = ""; + int port = 0; + if (requestURI.startsWith(StrUtil.SLASH + visual)) { + prefix = visual; + server = visualServer; + port = Integer.parseInt(visualPort); + } else if (requestURI.startsWith(StrUtil.SLASH + refine)) { + prefix = refine; + server = refineServer; + port = Integer.parseInt(refinePort); + } + + return new URI("http", null, server, port, requestURI.substring(prefix.length() + 1), request.getQueryString(), null); + } + + /** + * 获取请求中的Cookie + * + * @param request http请求 + * @return HttpHeaders 用于restTemplate的header + **/ + private HttpHeaders getHeader(HttpServletRequest request) { + String cookie = request.getHeader("Cookie"); + HttpHeaders httpHeaders = new HttpHeaders(); + if (null != cookie) { + httpHeaders.set("Cookie", cookie); + } + return httpHeaders; + } + + /** + * 转发get请求 + * + * @param request http请求 + * @return ResponseEntity 返回给前端的响应实体 + **/ + @GetMapping({StrUtil.SLASH + "${dubhe-proxy.visual.keyword}" + StrUtil.SLASH + "**", StrUtil.SLASH + "${dubhe-proxy.refine.keyword}" + StrUtil.SLASH + "**"}) + @ResponseBody + public ResponseEntity mirrorRest(HttpServletRequest request) throws URISyntaxException { + URI uri = getURI(request); + HttpHeaders httpHeaders = getHeader(request); + ResponseEntity responseEntity = + restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity(httpHeaders), String.class); + return responseEntity; + } + + /** + * 转发get请求 + * + * @param request http请求 + * @param method 请求方法 + * @param body 请求体 + * @return ResponseEntity 返回给前端的响应实体 + **/ + @RequestMapping({StrUtil.SLASH + "${dubhe-proxy.visual.keyword}" + StrUtil.SLASH + "**", StrUtil.SLASH + "${dubhe-proxy.refine.keyword}" + StrUtil.SLASH + "**"}) + @ResponseBody + public ResponseEntity mirrorRest(HttpMethod method, HttpServletRequest request, @RequestBody String body) throws URISyntaxException { + URI uri = getURI(request); + HttpHeaders httpHeaders = getHeader(request); + ResponseEntity responseEntity = + restTemplate.exchange(uri, method, new HttpEntity(body, httpHeaders), String.class); + return responseEntity; + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LogController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LogController.java new file mode 100644 index 0000000..089557d --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LogController.java @@ -0,0 +1,112 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.rest; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.LogQueryDTO; +import org.dubhe.admin.service.LogService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @description 日志管理 控制器 + * @date 2020-06-01 + */ +@Api(tags = "监控:日志管理") +@ApiIgnore +@RestController +@RequestMapping("/logs") +public class LogController { + + private final LogService logService; + + public LogController(LogService logService) { + this.logService = logService; + } + + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize(Permissions.SYSTEM_LOG) + public void download(HttpServletResponse response, LogQueryDTO criteria) throws IOException { + criteria.setLogType("INFO"); + logService.download(logService.queryAll(criteria), response); + } + + @ApiOperation("导出错误数据") + @GetMapping(value = "/error/download") + @PreAuthorize(Permissions.SYSTEM_LOG) + public void errorDownload(HttpServletResponse response, LogQueryDTO criteria) throws IOException { + criteria.setLogType("ERROR"); + logService.download(logService.queryAll(criteria), response); + } + + @GetMapping + @ApiOperation("日志查询") + @PreAuthorize(Permissions.SYSTEM_LOG) + public ResponseEntity getLogs(LogQueryDTO criteria, Page pageable) { + criteria.setLogType("INFO"); + return new ResponseEntity<>(logService.queryAll(criteria, pageable), HttpStatus.OK); + } + + @GetMapping(value = "/user") + @ApiOperation("用户日志查询") + public ResponseEntity getUserLogs(LogQueryDTO criteria, Page page) { + criteria.setLogType("INFO"); + criteria.setBlurry(JwtUtils.getCurUser().getUsername()); + return new ResponseEntity<>(logService.queryAllByUser(criteria, page), HttpStatus.OK); + } + + @GetMapping(value = "/error") + @ApiOperation("错误日志查询") + @PreAuthorize(Permissions.SYSTEM_LOG) + public ResponseEntity getErrorLogs(LogQueryDTO criteria, Page page) { + criteria.setLogType("ERROR"); + return new ResponseEntity<>(logService.queryAll(criteria, page), HttpStatus.OK); + } + + @GetMapping(value = "/error/{id}") + @ApiOperation("日志异常详情查询") + @PreAuthorize(Permissions.SYSTEM_LOG) + public ResponseEntity getErrorLogs(@PathVariable Long id) { + return new ResponseEntity<>(logService.findByErrDetail(id), HttpStatus.OK); + } + + @DeleteMapping(value = "/del/error") + @ApiOperation("删除所有ERROR日志") + @PreAuthorize(Permissions.SYSTEM_LOG) + public ResponseEntity delAllByError() { + logService.delAllByError(); + return new ResponseEntity<>(HttpStatus.OK); + } + + @DeleteMapping(value = "/del/info") + @ApiOperation("删除所有INFO日志") + @PreAuthorize(Permissions.SYSTEM_LOG) + public ResponseEntity delAllByInfo() { + logService.delAllByInfo(); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LoginController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LoginController.java new file mode 100644 index 0000000..6d9d37d --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/LoginController.java @@ -0,0 +1,175 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.rest; + +import cn.hutool.core.util.IdUtil; +import com.wf.captcha.SpecCaptcha; +import com.wf.captcha.base.Captcha; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.dubhe.admin.domain.dto.AuthUserDTO; +import org.dubhe.admin.domain.dto.UserRegisterDTO; +import org.dubhe.admin.domain.dto.UserRegisterMailDTO; +import org.dubhe.admin.domain.dto.UserResetPasswordDTO; +import org.dubhe.admin.service.UserService; +import org.dubhe.biz.base.constant.UserConstant; +import org.dubhe.biz.base.utils.DateUtil; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.redis.utils.RedisUtils; +import org.dubhe.cloud.authconfig.dto.JwtUserDTO; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * @description 系统登录 控制器 + * @date 2020-06-01 + */ +@Api(tags = "系统登录") +@RestController +@RequestMapping("/auth") +@Slf4j +@SuppressWarnings("unchecked") +public class LoginController { + + @Value("${rsa.public_key}") + private String publicKey; + + @Value("${loginCode.expiration}") + private Long expiration; + + @Value("${loginCode.width}") + private Integer width; + + @Value("${loginCode.height}") + private Integer height; + + @Value("${loginCode.length}") + private Integer length; + + @Value("${loginCode.codeKey}") + private String codeKey; + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private UserService userService; + + + @ApiOperation("登录") + @PostMapping(value = "/login") + public DataResponseBody> login(@Validated @RequestBody AuthUserDTO authUserDTO) { + return userService.login(authUserDTO); + } + + @ApiOperation("获取验证码") + @GetMapping(value = "/code") + public DataResponseBody getCode() { + Captcha captcha = new SpecCaptcha(width, height, length); + String createText = captcha.text(); + String uuid = codeKey + IdUtil.simpleUUID(); + // 保存 + redisUtils.set(uuid, createText, expiration, TimeUnit.MINUTES); + // 验证码信息 + Map imgResult = new HashMap(4) {{ + put("img", captcha.toBase64()); + put("uuid", uuid); + }}; + return new DataResponseBody(imgResult); + } + + + @ApiOperation("退出登录") + @DeleteMapping(value = "/logout") + public DataResponseBody logout(@RequestHeader("Authorization") String accessToken) { + return userService.logout(accessToken); + } + + @ApiOperation("获取用户信息") + @GetMapping(value = "/info") + public DataResponseBody info() { + JwtUserDTO curUser = JwtUtils.getCurUser(); + Set permissions = userService.queryPermissionByUserId(curUser.getCurUserId()); + Map authInfo = new HashMap(2) {{ + put("user", curUser.getUser()); + put("permissions", permissions); + }}; + return new DataResponseBody(authInfo); + } + + + @ApiOperation("用户注册信息") + @PostMapping(value = "/userRegister") + public DataResponseBody userRegister(@Valid @RequestBody UserRegisterDTO userRegisterDTO) { + return userService.userRegister(userRegisterDTO); + } + + + @ApiOperation("用户忘记密码") + @PostMapping(value = "/resetPassword") + public DataResponseBody resetPassword(@Valid @RequestBody UserResetPasswordDTO userResetPasswordDTO) { + return userService.resetPassword(userResetPasswordDTO); + } + + + @ApiOperation("获取code通过发送邮件") + @PostMapping(value = "/getCodeBySentEmail") + public DataResponseBody getCodeBySentEmail(@Valid @RequestBody UserRegisterMailDTO userRegisterMailDTO) { + return userService.getCodeBySentEmail(userRegisterMailDTO); + } + + + @ApiOperation("获取公钥") + @GetMapping(value = "/getPublicKey") + public DataResponseBody getPublicKey() { + return new DataResponseBody(publicKey); + } + + @ApiOperation(value = "获取用户信息 供第三方平台使用", notes = "获取用户信息 供第三方平台使用") + @GetMapping("/userinfo") + public Map userinfo() { + return userService.userinfo(); + } + + /** + * 限制登录失败次数 + * + * @param username + */ + private boolean limitLoginCount(final String username) { + String concat = UserConstant.USER_LOGIN_LIMIT_COUNT.concat(username); + double count = redisUtils.hincr(UserConstant.USER_LOGIN_LIMIT_COUNT.concat(username), concat, 1); + if (count > UserConstant.COUNT_LOGIN_FAIL) { + return false; + } else { + // 验证码次数凌晨清除 + long afterSixHourTime = DateUtil.getAfterSixHourTime(); + redisUtils.hset(concat, concat, afterSixHourTime); + } + return true; + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MailController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MailController.java new file mode 100644 index 0000000..97e4879 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MailController.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.service.MailService; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.mail.MessagingException; + +/** + * @description 邮件服务 + * @date 2021-01-21 + */ +@Api(tags = "系统:邮件服务") +@RestController +@RequestMapping("/mail") +public class MailController { + + @Resource + private MailService mailService; + + @ApiOperation("发送html邮件") + @PostMapping(value = "/sendHtmlMail") + public DataResponseBody sendHtmlMail(@RequestParam(value = "to") String to, @RequestParam(value = "subject") String subject, @RequestParam("content") String content) throws MessagingException { + mailService.sendHtmlMail(to, subject, content); + return new DataResponseBody(); + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MenuController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MenuController.java new file mode 100644 index 0000000..0d9d26c --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/MenuController.java @@ -0,0 +1,110 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.MenuCreateDTO; +import org.dubhe.admin.domain.dto.MenuDTO; +import org.dubhe.admin.domain.dto.MenuDeleteDTO; +import org.dubhe.admin.domain.dto.MenuQueryDTO; +import org.dubhe.admin.domain.dto.MenuUpdateDTO; +import org.dubhe.admin.domain.entity.Menu; +import org.dubhe.admin.service.MenuService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @description 菜单管理 控制器 + * @date 2020-06-01 + */ +@Api(tags = "系统:菜单管理") +@RestController +@RequestMapping("/menus") +@SuppressWarnings("unchecked") +public class MenuController { + + + @Autowired + private MenuService menuService; + + @ApiOperation("导出菜单数据") + @GetMapping(value = "/download") + @PreAuthorize(Permissions.MENU_DOWNLOAD) + public void download(HttpServletResponse response, MenuQueryDTO criteria) throws IOException { + menuService.download(menuService.queryAll(criteria), response); + } + + @ApiOperation("返回全部的菜单") + @GetMapping(value = "/tree") + public DataResponseBody getMenuTree() { + return new DataResponseBody(menuService.getMenuTree(menuService.findByPid(0L))); + } + + @ApiOperation("查询菜单") + @GetMapping + @PreAuthorize(Permissions.MENU) + public DataResponseBody getMenus(MenuQueryDTO criteria) { + List menuDtoList = menuService.queryAll(criteria); + return new DataResponseBody(menuService.buildTree(menuDtoList)); + } + + @ApiOperation("新增菜单") + @PostMapping + @PreAuthorize(Permissions.MENU_CREATE) + public DataResponseBody create(@Valid @RequestBody MenuCreateDTO resources) { + return new DataResponseBody(menuService.create(resources)); + } + + @ApiOperation("修改菜单") + @PutMapping + @PreAuthorize(Permissions.MENU_EDIT) + public DataResponseBody update(@Valid @RequestBody MenuUpdateDTO resources) { + menuService.update(resources); + return new DataResponseBody(); + } + + @ApiOperation("删除菜单") + @DeleteMapping + @PreAuthorize(Permissions.MENU_DELETE) + public DataResponseBody delete(@Valid @RequestBody MenuDeleteDTO deleteDTO) { + Set menuSet = new HashSet<>(); + Set ids = deleteDTO.getIds(); + for (Long id : ids) { + List menuList = menuService.findByPid(id); + menuSet.add(menuService.findOne(id)); + menuSet = menuService.getDeleteMenus(menuList, menuSet); + } + menuService.delete(menuSet); + return new DataResponseBody(); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/PermissionController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/PermissionController.java new file mode 100644 index 0000000..1621cc2 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/PermissionController.java @@ -0,0 +1,89 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.PermissionCreateDTO; +import org.dubhe.admin.domain.dto.PermissionDeleteDTO; +import org.dubhe.admin.domain.dto.PermissionQueryDTO; +import org.dubhe.admin.domain.dto.PermissionUpdateDTO; +import org.dubhe.admin.service.PermissionService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @description 操作权限控制器 + * @date 2021-04-26 + */ +@Api(tags = "系统:操作权限") +@RestController +@RequestMapping("/permission") +public class PermissionController { + + @Autowired + private PermissionService permissionService; + + @ApiOperation("返回全部的操作权限") + @GetMapping(value = "/tree") + public DataResponseBody getPermissionTree() { + return new DataResponseBody(permissionService.getPermissionTree(permissionService.findByPid(0L))); + } + + @ApiOperation("查询权限") + @GetMapping + @PreAuthorize(Permissions.AUTH_CODE) + public DataResponseBody queryAll(PermissionQueryDTO queryDTO) { + return new DataResponseBody(permissionService.queryAll(queryDTO)); + } + + @ApiOperation("新增权限") + @PostMapping + @PreAuthorize(Permissions.PERMISSION_CREATE) + public DataResponseBody create(@Validated @RequestBody PermissionCreateDTO permissionCreateDTO) { + permissionService.create(permissionCreateDTO); + return new DataResponseBody(); + } + + @ApiOperation("修改权限") + @PutMapping + @PreAuthorize(Permissions.PERMISSION_EDIT) + public DataResponseBody update(@Validated @RequestBody PermissionUpdateDTO permissionUpdateDTO) { + permissionService.update(permissionUpdateDTO); + return new DataResponseBody(); + } + + @ApiOperation("删除权限") + @DeleteMapping + @PreAuthorize(Permissions.PERMISSION_DELETE) + public DataResponseBody delete(@RequestBody PermissionDeleteDTO permissionDeleteDTO) { + permissionService.delete(permissionDeleteDTO); + return new DataResponseBody(); + } + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RecycleTaskController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RecycleTaskController.java new file mode 100644 index 0000000..9d595d7 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RecycleTaskController.java @@ -0,0 +1,79 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.service.RecycleTaskService; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.dubhe.recycle.domain.dto.RecycleTaskDeleteDTO; +import org.dubhe.recycle.domain.dto.RecycleTaskQueryDTO; +import org.dubhe.recycle.enums.RecycleModuleEnum; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @description 回收任务 + * @date 2020-09-23 + */ +@Api(tags = "系统:回收任务") +@RestController +@RequestMapping("/recycleTask") +public class RecycleTaskController { + + @Autowired + private RecycleTaskService recycleTaskService; + + @ApiOperation("查询回收任务列表") + @GetMapping + public DataResponseBody getRecycleTaskList(RecycleTaskQueryDTO recycleTaskQueryDTO) { + return DataResponseFactory.success(recycleTaskService.getRecycleTasks(recycleTaskQueryDTO)); + } + + @ApiOperation("(批量)立即删除") + @DeleteMapping + public DataResponseBody recycleTaskResources(@Validated @RequestBody RecycleTaskDeleteDTO recycleTaskDeleteDTO) { + for (long taskId:recycleTaskDeleteDTO.getRecycleTaskIdList()){ + recycleTaskService.recycleTaskResources(taskId); + } + return DataResponseFactory.successWithMsg("资源删除中"); + } + + @ApiOperation("获取模块代号,名称映射") + @GetMapping("/recycleModuleMap") + public DataResponseBody recycleModuleMap() { + return DataResponseFactory.success(RecycleModuleEnum.RECYCLE_MODULE_MAP); + } + + @ApiOperation("立即还原") + @PutMapping + public DataResponseBody restore(@RequestParam(required = true) long taskId) { + recycleTaskService.restore(taskId); + return DataResponseFactory.successWithMsg("还原成功"); + } + + + @ApiOperation("实时删除完整路径无效文件") + @DeleteMapping("/delTemp") + public DataResponseBody delTempInvalidResources(@RequestParam(required = true) String sourcePath) { + recycleTaskService.delTempInvalidResources(sourcePath); + return DataResponseFactory.successWithMsg("删除临时目录文件成功"); + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ResourceSpecsController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ResourceSpecsController.java new file mode 100644 index 0000000..b445977 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/ResourceSpecsController.java @@ -0,0 +1,82 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.ResourceSpecsCreateDTO; +import org.dubhe.admin.domain.dto.ResourceSpecsDeleteDTO; +import org.dubhe.admin.domain.dto.ResourceSpecsQueryDTO; +import org.dubhe.admin.domain.dto.ResourceSpecsUpdateDTO; +import org.dubhe.admin.service.ResourceSpecsService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.dto.QueryResourceSpecsDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.base.vo.QueryResourceSpecsVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * @description CPU, GPU, 内存等资源规格管理 + * @date 2021-05-27 + */ +@Api(tags = "系统:资源规格管理") +@RestController +@RequestMapping("/resourceSpecs") +public class ResourceSpecsController { + + @Autowired + private ResourceSpecsService resourceSpecsService; + + @ApiOperation("查询资源规格") + @GetMapping + public DataResponseBody getResourceSpecs(ResourceSpecsQueryDTO resourceSpecsQueryDTO) { + return new DataResponseBody(resourceSpecsService.getResourceSpecs(resourceSpecsQueryDTO)); + } + + @ApiOperation("查询资源规格(远程调用)") + @GetMapping("/queryResourceSpecs") + public DataResponseBody queryResourceSpecs(QueryResourceSpecsDTO queryResourceSpecsDTO) { + return new DataResponseBody(resourceSpecsService.queryResourceSpecs(queryResourceSpecsDTO)); + } + + @ApiOperation("新增资源规格") + @PostMapping + @PreAuthorize(Permissions.SPECS_CREATE) + public DataResponseBody create(@Valid @RequestBody ResourceSpecsCreateDTO resourceSpecsCreateDTO) { + return new DataResponseBody(resourceSpecsService.create(resourceSpecsCreateDTO)); + } + + @ApiOperation("修改资源规格") + @PutMapping + @PreAuthorize(Permissions.SPECS_EDIT) + public DataResponseBody update(@Valid @RequestBody ResourceSpecsUpdateDTO resourceSpecsUpdateDTO) { + return new DataResponseBody(resourceSpecsService.update(resourceSpecsUpdateDTO)); + } + + @ApiOperation("删除资源规格") + @DeleteMapping + @PreAuthorize(Permissions.SPECS_DELETE) + public DataResponseBody delete(@Valid @RequestBody ResourceSpecsDeleteDTO resourceSpecsDeleteDTO) { + resourceSpecsService.delete(resourceSpecsDeleteDTO); + return new DataResponseBody(); + } + +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RoleController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RoleController.java new file mode 100644 index 0000000..6099ad9 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/RoleController.java @@ -0,0 +1,142 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.rest; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.RoleAuthUpdateDTO; +import org.dubhe.admin.domain.dto.RoleCreateDTO; +import org.dubhe.admin.domain.dto.RoleDTO; +import org.dubhe.admin.domain.dto.RoleDeleteDTO; +import org.dubhe.admin.domain.dto.RoleQueryDTO; +import org.dubhe.admin.domain.dto.RoleUpdateDTO; +import org.dubhe.admin.service.AuthCodeService; +import org.dubhe.admin.service.RoleService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Objects; + + +/** + * @description 角色管理 控制器 + * @date 2020-06-01 + */ +@Api(tags = "系统:角色管理") +@RestController +@RequestMapping("/roles") +public class RoleController { + + private final RoleService roleService; + + @Autowired + private AuthCodeService authCodeService; + + public RoleController(RoleService roleService) { + this.roleService = roleService; + } + + @ApiOperation("查询角色") + @GetMapping + @PreAuthorize(Permissions.ROLE) + public DataResponseBody getRoles(RoleQueryDTO criteria, Page page) { + return new DataResponseBody(roleService.queryAll(criteria, page)); + } + + @ApiOperation("返回全部的角色") + @GetMapping(value = "/all") + @PreAuthorize(Permissions.ROLE) + public DataResponseBody getAll(RoleQueryDTO criteria) { + return new DataResponseBody(roleService.queryAllSmall(criteria)); + } + + @ApiOperation("获取单个role") + @GetMapping(value = "/{id}") + @PreAuthorize(Permissions.ROLE) + public DataResponseBody getRoles(@PathVariable Long id) { + return new DataResponseBody(roleService.findById(id)); + } + + @ApiOperation("新增角色") + @PostMapping + @PreAuthorize(Permissions.ROLE_CREATE) + public DataResponseBody create(@Valid @RequestBody RoleCreateDTO resources) { + return new DataResponseBody(roleService.create(resources)); + } + + @ApiOperation("修改角色") + @PutMapping + @PreAuthorize(Permissions.ROLE_EDIT) + public DataResponseBody update(@Valid @RequestBody RoleUpdateDTO resources) { + roleService.update(resources); + return new DataResponseBody(); + } + + @ApiOperation("修改角色菜单") + @PutMapping(value = "/menu") + @PreAuthorize(Permissions.ROLE_MENU) + public DataResponseBody updateMenu(@Valid @RequestBody RoleUpdateDTO resources) { + RoleDTO role = roleService.findById(resources.getId()); + if (Objects.isNull(role)) { + throw new BadCredentialsException("请选择角色信息"); + } + roleService.updateMenu(resources, role); + return new DataResponseBody(); + } + + @ApiOperation("修改角色权限") + @PutMapping(value = "/auth") + @PreAuthorize(Permissions.ROLE_AUTH) + public DataResponseBody updateAuth(@Valid @RequestBody RoleAuthUpdateDTO roleAuthUpdateDTO) { + RoleDTO role = roleService.findById(roleAuthUpdateDTO.getRoleId()); + if (Objects.isNull(role)) { + throw new BadCredentialsException("请选择角色信息"); + } + authCodeService.updateRoleAuth(roleAuthUpdateDTO); + return new DataResponseBody(); + } + + @ApiOperation("删除角色") + @DeleteMapping + @PreAuthorize(Permissions.ROLE_DELETE) + public DataResponseBody delete(@RequestBody RoleDeleteDTO deleteDTO) { + roleService.delete(deleteDTO.getIds()); + return new DataResponseBody(); + } + + @ApiOperation("导出角色数据") + @GetMapping(value = "/download") + @PreAuthorize(Permissions.ROLE_DWONLOAD) + public void download(HttpServletResponse response, RoleQueryDTO criteria) throws IOException { + roleService.download(roleService.queryAll(criteria), response); + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/TeamController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/TeamController.java new file mode 100644 index 0000000..ac072bd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/TeamController.java @@ -0,0 +1,96 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.TeamCreateDTO; +import org.dubhe.admin.domain.dto.TeamQueryDTO; +import org.dubhe.admin.domain.dto.TeamUpdateDTO; +import org.dubhe.admin.service.TeamService; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.base.constant.Permissions; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; + +/** + * @description 团队管理 控制器 + * @date 2020-06-01 + */ +@Api(tags = "系统:团队管理") +@ApiIgnore +@RestController +@RequestMapping("/teams") +public class TeamController { + private final TeamService teamService; + + public TeamController(TeamService teamService) { + this.teamService = teamService; + } + + @ApiOperation("获取单个团队信息") + @GetMapping(value = "/{id}") + @PreAuthorize(Permissions.SYSTEM_TEAM) + public DataResponseBody getTeam(@PathVariable Long id) { + return new DataResponseBody(teamService.findById(id)); + } + + @ApiOperation("导出团队数据") + @GetMapping(value = "/download") + @PreAuthorize(Permissions.SYSTEM_TEAM) + public void download(HttpServletResponse response, TeamQueryDTO criteria) throws IOException { + teamService.download(teamService.queryAll(criteria), response); + } + + @ApiOperation("返回全部的团队") + @GetMapping(value = "/all") + @PreAuthorize(Permissions.SYSTEM_TEAM) + public DataResponseBody getAll(@PageableDefault(value = 2000, sort = {"level"}, direction = Sort.Direction.ASC) Page page) { + return new DataResponseBody(teamService.queryAll(page)); + } + + @ApiOperation("查询团队") + @GetMapping + @PreAuthorize(Permissions.SYSTEM_TEAM) + public DataResponseBody getTeams(TeamQueryDTO criteria, Page page) { + System.out.println("com here"); + return new DataResponseBody(teamService.queryAll(criteria, page)); + } + + @ApiOperation("新增团队") + @PostMapping + @PreAuthorize(Permissions.SYSTEM_TEAM) + public DataResponseBody create(@Valid @RequestBody TeamCreateDTO resources) { + return new DataResponseBody(teamService.create(resources)); + } + + @ApiOperation("修改团队") + @PutMapping + @PreAuthorize(Permissions.SYSTEM_TEAM) + public DataResponseBody update(@Valid @RequestBody TeamUpdateDTO resources) { + teamService.update(resources); + return new DataResponseBody(); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserCenterController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserCenterController.java new file mode 100644 index 0000000..2188eae --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserCenterController.java @@ -0,0 +1,140 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.rest; + +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.service.DictService; +import org.dubhe.admin.service.MenuService; +import org.dubhe.admin.service.RoleService; +import org.dubhe.admin.service.UserService; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.cloud.authconfig.factory.PasswordEncoderFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * @description 个人中心 控制器 + * @date 2020-06-01 + */ +@Api(tags = "系统:个人中心") +@RestController +@RequestMapping("/user") +public class UserCenterController { + + @Autowired + private UserService userService; + + @Autowired + private RoleService roleService; + + @Autowired + private MenuService menuService; + + @Autowired + private DictService dictService; + + @Autowired + private UserContextService userContextService; + + @Value("${rsa.private_key}") + private String privateKey; + + + @ApiOperation("用户团队") + @GetMapping(value = "/teams") + public DataResponseBody getTeams() { + return new DataResponseBody(userService.queryTeams(userContextService.getCurUserId())); + } + + @ApiOperation("获取前端所需菜单") + @GetMapping(value = "/menus") + public DataResponseBody menus() { + Long curUserId = userContextService.getCurUserId(); + List roles = roleService.getRoleByUserId(curUserId); + List menuDtoList = menuService.findByRoles(roles); + List menuDtos = (List) menuService.buildTree(menuDtoList).get("result"); + + return new DataResponseBody(menuService.buildMenus(menuDtos)); + } + + @ApiOperation("用户查询字典详情") + @GetMapping(value = "/dict/{name}") + public DataResponseBody getDict(@PathVariable String name) { + return new DataResponseBody(dictService.findByName(name)); + } + + @ApiOperation("个人中心") + @GetMapping(value = "info") + public DataResponseBody getInfo() { + return new DataResponseBody(userContextService.getCurUser()); + } + + @ApiOperation("修改用户:个人中心") + @PutMapping(value = "info") + public DataResponseBody info(@Valid @RequestBody UserCenterUpdateDTO resources) { + Long curUserId = userContextService.getCurUserId(); + if (!resources.getId().equals(curUserId)) { + throw new BusinessException("不能修改他人资料"); + } + userService.updateCenter(resources); + return new DataResponseBody(userService.findById(curUserId)); + } + + @ApiOperation("修改密码") + @PostMapping(value = "/updatePass") + public DataResponseBody updatePass(@Valid @RequestBody UserPassUpdateDTO passUpdateDTO) { + PasswordEncoder passwordEncoder = PasswordEncoderFactory.getPasswordEncoder(); + // 密码解密 + RSA rsa = new RSA(privateKey, null); + String oldPass = new String(rsa.decrypt(passUpdateDTO.getOldPass(), KeyType.PrivateKey)); + String newPass = new String(rsa.decrypt(passUpdateDTO.getNewPass(), KeyType.PrivateKey)); + UserContext curUser = userContextService.getCurUser(); + if (!passwordEncoder.matches(oldPass, curUser.getPassword())) { + throw new BusinessException("修改失败,旧密码错误"); + } + if (passwordEncoder.matches(newPass, curUser.getPassword())) { + throw new BusinessException("新密码不能与旧密码相同"); + } + userService.updatePass(curUser.getUsername(), passwordEncoder.encode(newPass)); + return new DataResponseBody(); + } + + @ApiOperation("修改头像") + @PostMapping(value = "/updateAvatar") + public DataResponseBody updateAvatar(@Valid @RequestBody UserAvatarUpdateDTO avatarUpdateDTO) { + userService.updateAvatar(avatarUpdateDTO.getRealName(), avatarUpdateDTO.getPath()); + return new DataResponseBody(); + } + + + @ApiOperation("修改邮箱接口") + @PostMapping(value = "/resetEmail") + public DataResponseBody resetEmail(@RequestBody UserEmailUpdateDTO userEmailUpdateDTO) { + return userService.resetEmail(userEmailUpdateDTO); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserController.java new file mode 100644 index 0000000..ef70bda --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserController.java @@ -0,0 +1,118 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.UserCreateDTO; +import org.dubhe.admin.domain.dto.UserDeleteDTO; +import org.dubhe.admin.domain.dto.UserQueryDTO; +import org.dubhe.admin.domain.dto.UserUpdateDTO; +import org.dubhe.admin.service.UserService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.UserDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +/** + * @description 用户管理 + * @date 2020-11-03 + */ +@Api(tags = "系统:用户管理") +@RestController +@RequestMapping("/users") +public class UserController { + + @Autowired + private UserService userService; + + @ApiOperation("导出用户数据") + @GetMapping(value = "/download") + @PreAuthorize(Permissions.USER_DOWNLOAD) + public void download(HttpServletResponse response, UserQueryDTO criteria) throws IOException { + userService.download(userService.queryAll(criteria), response); + } + + @ApiOperation("查询用户") + @GetMapping + public DataResponseBody getUsers(UserQueryDTO criteria, Page page) { + return new DataResponseBody(userService.queryAll(criteria, page)); + } + + @ApiOperation("新增用户") + @PostMapping + @PreAuthorize(Permissions.USER_CREATE) + public DataResponseBody create(@Valid @RequestBody UserCreateDTO userCreateDTO) { + return new DataResponseBody(userService.create(userCreateDTO)); + } + + @ApiOperation("修改用户") + @PutMapping + @PreAuthorize(Permissions.USER_EDIT) + public DataResponseBody update(@Valid @RequestBody UserUpdateDTO resources) { + return new DataResponseBody(userService.update(resources)); + } + + @ApiOperation("删除用户") + @DeleteMapping + @PreAuthorize(Permissions.USER_DELETE) + public DataResponseBody delete(@Valid @RequestBody UserDeleteDTO userDeleteDTO) { + userService.delete(userDeleteDTO.getIds()); + return new DataResponseBody(); + } + + /** + * 此接口提供给Auth模块获取用户信息使用 + * 因Auth获取用户信息在登录时是未登录状态,请不要在此添加权限校验 + * @param username + * @return + */ + @ApiOperation("根据用户名称查找用户") + @GetMapping(value = "/findUserByUsername") + public DataResponseBody findUserByUsername(@RequestParam(value = "username") String username) { + DataResponseBody userContextDataResponseBody = userService.findUserByUsername(username); + return userContextDataResponseBody; + } + + + @ApiOperation("根据用户ID查询用户信息(服务内部访问)") + @GetMapping(value = "/findById") + public DataResponseBody getUsers(@RequestParam(value = "userId") Long userId) { + return new DataResponseBody(userService.findById(userId)); + } + + @ApiOperation("根据用户昵称搜索用户列表") + @GetMapping(value = "/findByNickName") + public DataResponseBody> findByNickName(@RequestParam(value = "nickName",required = false) String nickName) { + return new DataResponseBody(userService.findByNickName(nickName)); + } + + @ApiOperation("根据用户ID批量查询用户信息(服务内部访问)") + @GetMapping(value = "/findByIds") + public DataResponseBody> getUserList(@RequestParam(value = "ids") List ids) { + return new DataResponseBody(userService.getUserList(ids)); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserGroupController.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserGroupController.java new file mode 100644 index 0000000..65d2759 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/rest/UserGroupController.java @@ -0,0 +1,133 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.admin.domain.dto.UserGroupDTO; +import org.dubhe.admin.domain.dto.UserGroupDeleteDTO; +import org.dubhe.admin.domain.dto.UserGroupQueryDTO; +import org.dubhe.admin.domain.dto.UserGroupUpdDTO; +import org.dubhe.admin.domain.dto.UserRoleUpdateDTO; +import org.dubhe.admin.domain.dto.UserStateUpdateDTO; +import org.dubhe.admin.service.UserGroupService; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @description 用户组管理 控制器 + * @date 2021-05-10 + */ +@Api(tags = "系统:用户组管理") +@RestController +@RequestMapping("/group") +public class UserGroupController { + + @Autowired + private UserGroupService userGroupService; + + @GetMapping + @ApiOperation("获取用户组列表") + public DataResponseBody queryAll(UserGroupQueryDTO queryDTO) { + return new DataResponseBody(userGroupService.queryAll(queryDTO)); + } + + @PostMapping + @ApiOperation("创建用户组") + @PreAuthorize(Permissions.USER_GROUP_CREATE) + public DataResponseBody create(@Validated @RequestBody UserGroupDTO groupCreateDTO) { + return new DataResponseBody(userGroupService.create(groupCreateDTO)); + } + + @PutMapping + @ApiOperation("修改用户组信息") + @PreAuthorize(Permissions.USER_GROUP_EDIT) + public DataResponseBody update(@Validated @RequestBody UserGroupDTO groupUpdateDTO) { + userGroupService.update(groupUpdateDTO); + return new DataResponseBody(); + } + + @DeleteMapping + @ApiOperation("删除用户组") + @PreAuthorize(Permissions.USER_GROUP_DELETE) + public DataResponseBody delete(@RequestBody UserGroupDeleteDTO groupDeleteDTO) { + userGroupService.delete(groupDeleteDTO.getIds()); + return new DataResponseBody(); + } + + @PostMapping("/updateUser") + @ApiOperation("修改组成员") + @PreAuthorize(Permissions.USER_GROUP_EDIT_USER) + public DataResponseBody updUserWithGroup(@Validated @RequestBody UserGroupUpdDTO userGroupUpdDTO) { + userGroupService.updUserWithGroup(userGroupUpdDTO); + return new DataResponseBody(); + } + + @DeleteMapping("/deleteUser") + @ApiOperation("移除组成员") + @PreAuthorize(Permissions.USER_GROUP_EDIT_USER) + public DataResponseBody delUserWithGroup(@Validated @RequestBody UserGroupUpdDTO userGroupDelDTO) { + userGroupService.delUserWithGroup(userGroupDelDTO); + return new DataResponseBody(); + } + + @GetMapping("/findUser") + @ApiOperation("获取没有归属组的用户") + public DataResponseBody findUserWithOutGroup() { + return new DataResponseBody(userGroupService.findUserWithOutGroup()); + } + + @GetMapping("/byGroupId") + @ApiOperation("获取某个用户组的成员") + public DataResponseBody queryUserByGroupId(Long groupId) { + return new DataResponseBody(userGroupService.queryUserByGroupId(groupId)); + } + + @PutMapping("/userState") + @ApiOperation("批量修改组用户的状态(激活/锁定)") + @PreAuthorize(Permissions.USER_GROUP_EDIT_USER_STATE) + public DataResponseBody updateUserState(@Validated @RequestBody UserStateUpdateDTO userStateUpdateDTO) { + userGroupService.updateUserState(userStateUpdateDTO); + return new DataResponseBody(); + } + + @DeleteMapping("/delete") + @ApiOperation("批量删除组用户") + @PreAuthorize(Permissions.USER_GROUP_DELETE_USER) + public DataResponseBody delUser(@Validated @RequestBody UserGroupUpdDTO userGroupUpdDTO) { + userGroupService.delUser(userGroupUpdDTO); + return new DataResponseBody(); + } + + @PutMapping("/userRoles") + @ApiOperation("批量修改组成员角色") + @PreAuthorize(Permissions.USER_GROUP_EDIT_USER_ROLE) + public DataResponseBody updateUserRole(@Validated @RequestBody UserRoleUpdateDTO userRoleUpdateDTO) { + userGroupService.updateUserRole(userRoleUpdateDTO); + return new DataResponseBody(); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/AuthCodeService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/AuthCodeService.java new file mode 100644 index 0000000..77c22f4 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/AuthCodeService.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.dubhe.admin.domain.dto.AuthCodeCreateDTO; +import org.dubhe.admin.domain.dto.AuthCodeQueryDTO; +import org.dubhe.admin.domain.dto.AuthCodeUpdateDTO; +import org.dubhe.admin.domain.dto.RoleAuthUpdateDTO; +import org.dubhe.admin.domain.entity.Auth; +import org.dubhe.admin.domain.vo.AuthVO; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description 权限组服务类 + * @date 2021-05-14 + */ +public interface AuthCodeService extends IService { + + /** + * 分页查询权限组信息 + * + * @param authCodeQueryDTO 分页查询实例 + * @return Map 权限组分页信息 + */ + Map queryAll(AuthCodeQueryDTO authCodeQueryDTO); + + /** + * 创建权限组 + * + * @param authCodeCreateDTO 创建权限组DTO实例 + */ + void create(AuthCodeCreateDTO authCodeCreateDTO); + + /** + * 修改权限组信息 + * + * @param authCodeUpdateDTO 修改权限组信息DTO实例 + */ + void update(AuthCodeUpdateDTO authCodeUpdateDTO); + + /** + * 批量删除权限组 + * + * @param ids 权限组id集合 + */ + void delete(Set ids); + + /** + * 修改角色-权限组绑定关系 + * + * @param roleAuthUpdateDTO 角色-权限组关系映射DTO实例 + */ + void updateRoleAuth(RoleAuthUpdateDTO roleAuthUpdateDTO); + + /** + * 获取权限组列表 + * + * @return List 权限组列表信息 + */ + List getAuthCodeList(); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DataSequenceService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DataSequenceService.java new file mode 100644 index 0000000..3860a48 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DataSequenceService.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service; + +import org.dubhe.admin.domain.entity.DataSequence; + +/** + * @description 获取序列服务接口 + * @date 2020-09-23 + */ +public interface DataSequenceService { + /** + * 根据业务编码获取序列号 + * @param businessCode 业务编码 + * @return DataSequence 序列实体 + */ + DataSequence getSequence(String businessCode); + /** + * 根据业务编码更新起点 + * @param businessCode 业务编码 + * @return DataSequence 序列实体 + */ + int updateSequenceStart(String businessCode); + + /** + * 检查表是否存在 + * @param tableName 表名 + * @return boolean 是否存在标识 + */ + boolean checkTableExist(String tableName); + + /** + * 执行存储过程创建表 + * @param tableId 表名 + */ + void createTable(String tableId); + + /** + * 扩容可用数量 + * + * @param businessCode 业务编码 + * @return DataSequence 数据ID序列 + */ + DataSequence expansionUsedNumber(String businessCode); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictDetailService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictDetailService.java new file mode 100644 index 0000000..3489cab --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictDetailService.java @@ -0,0 +1,89 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.entity.DictDetail; +import org.dubhe.biz.base.dto.DictDetailQueryByLabelNameDTO; +import org.dubhe.biz.base.vo.DictDetailVO; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description 字典详情服务 Service + * @date 2020-06-01 + */ +public interface DictDetailService { + + /** + * 根据ID查询 + * + * @param id 字典Id + * @return 字典详情DTO + */ + DictDetailDTO findById(Long id); + + /** + * 创建 + * + * @param resources 字典详情创建实体 + * @return 字典详情实体 + */ + DictDetailDTO create(DictDetailCreateDTO resources); + + /** + * 编辑 + * + * @param resources 字典详情修改实体 + */ + void update(DictDetailUpdateDTO resources); + + /** + * 删除 + * + * @param ids 字典详情ids + */ + void delete(Set ids); + + /** + * 分页查询 + * + * @param criteria 条件 + * @param page 分页参数 + * @return 字典分页列表数据 + */ + Map queryAll(DictDetailQueryDTO criteria, Page page); + + /** + * 查询全部数据 + * + * @param criteria 字典查询实体 + * @return 字典列表数据 + */ + List queryAll(DictDetailQueryDTO criteria); + + /** + * 根据名称查询字典详情 + * + * @param dictDetailQueryByLabelNameDTO 字典名称 + * @return List 字典集合 + */ + List getDictName(DictDetailQueryByLabelNameDTO dictDetailQueryByLabelNameDTO); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictService.java new file mode 100644 index 0000000..5f2b148 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/DictService.java @@ -0,0 +1,101 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.domain.dto.DictCreateDTO; +import org.dubhe.admin.domain.dto.DictDTO; +import org.dubhe.admin.domain.dto.DictQueryDTO; +import org.dubhe.admin.domain.dto.DictUpdateDTO; +import org.dubhe.admin.domain.entity.Dict; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description 字典服务 Service + * @date 2020-06-01 + */ +public interface DictService { + + /** + * 分页查询 + * + * @param criteria 条件 + * @param page 分页参数 + * @return / + */ + Map queryAll(DictQueryDTO criteria, Page page); + + /** + * 按条件查询字典列表 + * + * @param criteria 字典查询实体 + * @return java.util.List 字典实例 + */ + List queryAll(DictQueryDTO criteria); + + /** + * 通过ID查询字典详情 + * + * @param id 字典ID + * @return org.dubhe.domain.dto.DictDTO 字典实例 + */ + DictDTO findById(Long id); + + /** + * 通过Name查询字典详情 + * + * @param name 字典名称 + * @return org.dubhe.domain.dto.DictDTO 字典实例 + */ + DictDTO findByName(String name); + + /** + * 新增字典 + * + * @param resources 字典新增实体 + * @return org.dubhe.domain.dto.DictDTO 字典实例 + */ + DictDTO create(DictCreateDTO resources); + + /** + * 字典修改 + * + * @param resources 字典修改实体 + */ + void update(DictUpdateDTO resources); + + /** + * 字典批量删除 + * + * @param ids 字典ID + */ + void deleteAll(Set ids); + + /** + * 导出数据 + * + * @param queryAll 待导出的数据 + * @param response 导出http响应 + * @throws IOException 导出异常 + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/LogService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/LogService.java new file mode 100644 index 0000000..8c1fcbd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/LogService.java @@ -0,0 +1,95 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.domain.dto.LogQueryDTO; +import org.dubhe.admin.domain.entity.Log; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * @description 日志服务 Service + * @date 2020-06-01 + */ +public interface LogService { + + /** + * 分页查询 + * + * @param criteria 查询条件 + * @param page 分页参数 + * @return Object 分页查询响应 + */ + Object queryAll(LogQueryDTO criteria, Page page); + + /** + * 查询全部数据 + * + * @param criteria 查询条件 + * @return 日志列表 + */ + List queryAll(LogQueryDTO criteria); + + /** + * 查询用户日志 + * + * @param criteria 查询条件 + * @param page 分页参数 + * @return 日志 + */ + Object queryAllByUser(LogQueryDTO criteria, Page page); + + /** + * 保存日志数据 + * @param username 用户 + * @param browser 浏览器 + * @param ip 请求IP + * @param joinPoint / + * @param log 日志实体 + */ + //@Async + //void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, Log log); + + /** + * 查询异常详情 + * + * @param id 日志ID + * @return Object 日志详情 + */ + Object findByErrDetail(Long id); + + /** + * 导出日志 + * + * @param logs 待导出的数据 + * @param response 导出http响应 + * @throws IOException 导出异常 + */ + void download(List logs, HttpServletResponse response) throws IOException; + + /** + * 删除所有错误日志 + */ + void delAllByError(); + + /** + * 删除所有INFO日志 + */ + void delAllByInfo(); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/MailService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/MailService.java new file mode 100644 index 0000000..52a1c5c --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/MailService.java @@ -0,0 +1,74 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service; + +import javax.mail.MessagingException; + +/** + * @description 邮箱服务 Service + * @date 2020-06-01 + */ +public interface MailService { + + /** + * 简单文本邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param contnet 邮件内容 + */ + void sendSimpleMail(String to, String subject, String contnet); + + /** + * HTML 文本邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param content HTML内容 + * @throws MessagingException + */ + void sendHtmlMail(String to, String subject, String content) throws MessagingException; + + /** + * 附件邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param content HTML内容 + * @param filePath 附件路径 + * @throws MessagingException + */ + void sendAttachmentsMail(String to, String subject, String content, + String filePath) throws MessagingException; + + + + /** + * 图片邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param content HTML内容 + * @param rscPath 图片路径 + * @param rscId 图片ID + * @throws MessagingException + */ + void sendLinkResourceMail(String to, String subject, String content, + String rscPath, String rscId); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/MenuService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/MenuService.java new file mode 100644 index 0000000..90bd1ba --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/MenuService.java @@ -0,0 +1,136 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service; + +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.entity.Menu; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description 菜单服务 Service + * @date 2020-06-01 + */ +public interface MenuService { + + /** + * 按条件查询菜单列表 + * + * @param criteria 菜单请求实体 + * @return java.util.List 菜单返回实例 + */ + List queryAll(MenuQueryDTO criteria); + + /** + * 根据id查询菜单信息 + * + * @param id 菜单id + * @return org.dubhe.domain.dto.MenuDTO 菜单返回实例 + */ + MenuDTO findById(long id); + + /** + * 新增菜单 + * + * @param resources 菜单新增请求实体 + * @return org.dubhe.domain.dto.MenuDTO 菜单返回实例 + */ + MenuDTO create(MenuCreateDTO resources); + + /** + * 修改菜单 + * + * @param resources 菜单修改请求实体 + */ + void update(MenuUpdateDTO resources); + + /** + * 查询可删除的菜单 + * + * @param menuList + * @param menuSet + * @return java.util.Set + */ + Set getDeleteMenus(List menuList, Set menuSet); + + /** + * 获取菜单树 + * + * @param menus 菜单列表 + * @return java.lang.Object 菜单树结构列表 + */ + Object getMenuTree(List menus); + + /** + * 根据ID获取菜单列表 + * + * @param pid id + * @return java.util.List 菜单返回列表 + */ + List findByPid(long pid); + + /** + * 构建菜单树 + * + * @param menuDtos 菜单请求实体 + * @return java.util.Map 菜单树结构 + */ + Map buildTree(List menuDtos); + + /** + * 根据角色查询菜单列表 + * + * @param roles 角色 + * @return java.util.List 菜单返回实例 + */ + List findByRoles(List roles); + + /** + * 构建菜单树 + * + * @param menuDtos 菜单请求实体 + * @return java.util.List 菜单树返回实例 + */ + Object buildMenus(List menuDtos); + + /** + * 获取菜单 + * + * @param id id + * @return org.dubhe.domain.entity.Menu 菜单返回实例 + */ + Menu findOne(Long id); + + /** + * 删除菜单 + * + * @param menuSet 删除菜单请求集合 + */ + void delete(Set menuSet); + + /** + * 导出 + * + * @param queryAll 待导出的数据 + * @param response 导出http响应 + * @throws IOException 导出异常 + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/PermissionService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/PermissionService.java new file mode 100644 index 0000000..cffe68b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/PermissionService.java @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.dubhe.admin.domain.dto.PermissionCreateDTO; +import org.dubhe.admin.domain.dto.PermissionDeleteDTO; +import org.dubhe.admin.domain.dto.PermissionQueryDTO; +import org.dubhe.admin.domain.dto.PermissionUpdateDTO; +import org.dubhe.admin.domain.entity.Permission; + +import java.util.List; +import java.util.Map; + +/** + * @description 操作权限服务 + * @date 2021-04-28 + */ +public interface PermissionService extends IService { + + + /** + * 根据ID获取权限列表 + * + * @param pid id + * @return java.util.List 权限返回列表 + */ + List findByPid(long pid); + + /** + * 获取权限树 + * + * @param permissions 权限列表 + * @return java.lang.Object 权限树树结构列表 + */ + Object getPermissionTree(List permissions); + + + /** + * 获取权限列表 + * + * @param permissionQueryDTO 权限查询DTO + * @return Map + */ + Map queryAll(PermissionQueryDTO permissionQueryDTO); + + /** + * 新增权限 + * + * @param permissionCreateDTO 新增权限DTO + */ + void create(PermissionCreateDTO permissionCreateDTO); + + /** + * 修改权限 + * + * @param permissionUpdateDTO 修改权限DTO + */ + void update(PermissionUpdateDTO permissionUpdateDTO); + + /** + * 删除权限 + * + * @param permissionDeleteDTO 删除权限DTO + */ + void delete(PermissionDeleteDTO permissionDeleteDTO); + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/RecycleTaskService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/RecycleTaskService.java new file mode 100644 index 0000000..fe8f8f7 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/RecycleTaskService.java @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import org.dubhe.recycle.domain.dto.RecycleTaskQueryDTO; +import org.dubhe.recycle.domain.entity.Recycle; + +import java.util.List; +import java.util.Map; + +/** + * @description 垃圾回收 + * @date 2021-02-23 + */ +public interface RecycleTaskService { + + + /** + * 查询回收任务列表 + * + * @param recycleTaskQueryDTO 查询任务列表条件 + * @return Map 可回收任务列表 + */ + Map getRecycleTasks(RecycleTaskQueryDTO recycleTaskQueryDTO); + + + /** + * 获取垃圾回收任务列表 + * 资源回收单次执行任务数量限制(默认10000) + * @return List 垃圾回收任务列表 + */ + List getRecycleTaskList(); + + /** + * 执行回收任务(单个) + * @param recycle 回收实体类 + * @param userId 当前操作用户 + */ + void recycleTask(Recycle recycle,long userId); + + + /** + * 实时删除临时目录无效文件 + * + * @param sourcePath 删除路径 + */ + void delTempInvalidResources(String sourcePath); + + /** + * 立即执行回收任务 + * + * @param taskId 回收任务ID + */ + void recycleTaskResources(long taskId); + + /** + * 还原回收任务 + * + * @param taskId 回收任务ID + */ + void restore(long taskId); + + /** + * 根据路径回收无效文件 + * + * @param sourcePath 文件路径 + */ + void deleteInvalidResourcesByCMD(String sourcePath); + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/ResourceSpecsService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/ResourceSpecsService.java new file mode 100644 index 0000000..998d387 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/ResourceSpecsService.java @@ -0,0 +1,65 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import org.dubhe.admin.domain.dto.*; +import org.dubhe.biz.base.vo.QueryResourceSpecsVO; +import org.dubhe.biz.base.dto.QueryResourceSpecsDTO; + +import java.util.List; +import java.util.Map; + +/** + * @description CPU, GPU, 内存等资源规格管理 + * @date 2021-05-27 + */ +public interface ResourceSpecsService { + + /** + * 查询资源规格 + * @param resourceSpecsQueryDTO 查询资源规格请求实体 + * @return List resourceSpecs 资源规格列表 + */ + Map getResourceSpecs(ResourceSpecsQueryDTO resourceSpecsQueryDTO); + + /** + * 新增资源规格 + * @param resourceSpecsCreateDTO 新增资源规格实体 + * @return List 新增资源规格id + */ + List create(ResourceSpecsCreateDTO resourceSpecsCreateDTO); + + /** + * 修改资源规格 + * @param resourceSpecsUpdateDTO 修改资源规格实体 + * @return List 修改资源规格id + */ + List update(ResourceSpecsUpdateDTO resourceSpecsUpdateDTO); + + /** + * 资源规格删除 + * @param resourceSpecsDeleteDTO 资源规格删除id集合 + */ + void delete(ResourceSpecsDeleteDTO resourceSpecsDeleteDTO); + + /** + * 查询资源规格 + * @param queryResourceSpecsDTO 查询资源规格请求实体 + * @return QueryResourceSpecsVO 资源规格返回结果实体类 + */ + QueryResourceSpecsVO queryResourceSpecs(QueryResourceSpecsDTO queryResourceSpecsDTO); +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/RoleService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/RoleService.java new file mode 100644 index 0000000..2000e30 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/RoleService.java @@ -0,0 +1,141 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.dto.RoleDTO; +import org.dubhe.admin.domain.entity.Role; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * @description 角色服务 Service + * @date 2020-06-01 + */ +public interface RoleService extends IService { + + /** + * 根据ID查询角色信息 + * + * @param id id + * @return org.dubhe.domain.dto.RoleDTO 角色信息 + */ + RoleDTO findById(long id); + + /** + * 新增角色 + * + * @param resources 角色新增请求实体 + * @return org.dubhe.domain.dto.RoleDTO 角色返回实例 + */ + RoleDTO create(RoleCreateDTO resources); + + /** + * 修改角色 + * + * @param resources 角色修改请求实体 + * @return void + */ + void update(RoleUpdateDTO resources); + + /** + * 批量删除角色 + * + * @param ids 角色ids + */ + void delete(Set ids); + + + /** + * 修改角色菜单 + * + * @param resources 角色菜单请求实体 + * @param roleDTO 角色请求实体 + */ + void updateMenu(RoleUpdateDTO resources, RoleDTO roleDTO); + + + /** + * 删除角色菜单 + * + * @param id 角色id + */ + void untiedMenu(Long id); + + /** + * 按条件查询角色信息 + * + * @param criteria 角色查询条件 + * @return java.util.List 角色信息返回实例 + */ + List queryAllSmall(RoleQueryDTO criteria); + + /** + * 分页查询角色列表 + * + * @param criteria 角色查询条件 + * @param page 分页实体 + * @return java.lang.Object 角色信息返回实例 + */ + Object queryAll(RoleQueryDTO criteria, Page page); + + /** + * 按条件查询角色列表 + * + * @param criteria 角色查询条件 + * @return java.util.List 角色信息返回实例 + */ + List queryAll(RoleQueryDTO criteria); + + /** + * 导出角色信息 + * + * @param roles 角色列表 + * @param response 导出http响应 + */ + void download(List roles, HttpServletResponse response) throws IOException; + + /** + * 根据用户ID获取角色信息 + * + * @param userId 用户ID + * @return java.util.List 角色列表 + */ + List getRoleByUserId( Long userId); + + /** + * 获取角色列表 + * + * @param userId 用户ID + * @param teamId 团队ID + * @return java.util.List 角色列表 + */ + List getRoleByUserIdAndTeamId( Long userId,Long teamId); + + + /** + * 新增角色菜单 + * + * @param roleId 角色ID + * @param menuId 菜单ID + */ + void tiedRoleMenu(Long roleId, Long menuId); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/TeamService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/TeamService.java new file mode 100644 index 0000000..560dab1 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/TeamService.java @@ -0,0 +1,102 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.domain.dto.TeamCreateDTO; +import org.dubhe.biz.base.dto.TeamDTO; +import org.dubhe.admin.domain.dto.TeamQueryDTO; +import org.dubhe.admin.domain.dto.TeamUpdateDTO; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * @description 团队服务 Service + * @date 2020-06-01 + */ +public interface TeamService { + + /** + * 获取团队列表 + * + * @param criteria 查询团队列表条件 + * @return java.util.List 团队列表条件 + */ + List queryAll(TeamQueryDTO criteria); + + /** + * 查询团队列表 + * + * @param page 分页请求实体 + * @return java.util.List 团队列表 + */ + List queryAll(Page page); + + /** + * 分页查询团队列表 + * + * @param criteria 查询请求条件 + * @param page 分页实体 + * @return java.lang.Object 团队列表 + */ + Object queryAll(TeamQueryDTO criteria, Page page); + + /** + * 根据ID插叙团队信息 + * + * @param id id + * @return org.dubhe.domain.dto.TeamDTO 团队返回实例 + */ + TeamDTO findById(Long id); + + /** + * 新增团队信息 + * + * @param resources 团队新增请求实体 + * @return org.dubhe.domain.dto.TeamDTO 团队返回实例 + */ + TeamDTO create(TeamCreateDTO resources); + + /** + * 修改团队 + * + * @param resources 团队修改请求实体 + */ + void update(TeamUpdateDTO resources); + + /** + * 团队删除 + * + * @param deptDtos 团队删除列表 + */ + void delete(Set deptDtos); + + + + /** + * 团队信息导出 + * + * @param teamDtos 团队列表 + * @param response 导出http响应 + */ + void download(List teamDtos, HttpServletResponse response) throws IOException; + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserGroupService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserGroupService.java new file mode 100644 index 0000000..b89f466 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserGroupService.java @@ -0,0 +1,112 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.entity.Group; +import org.dubhe.admin.domain.entity.User; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description 用户组服务类 + * @date 2021-05-06 + */ +public interface UserGroupService { + + + /** + * 分页查询用户组列表 + * + * @param queryDTO 查询实体DTO + * @return Map 用户组及分页信息 + */ + Map queryAll(UserGroupQueryDTO queryDTO); + + /** + * 新增用户组 + * + * @param groupCreateDTO 新增用户组实体DTO + */ + Group create(UserGroupDTO groupCreateDTO); + + /** + * 修改用户组信息 + * + * @param groupUpdateDTO 修改用户组实体DTO + */ + void update(UserGroupDTO groupUpdateDTO); + + /** + * 删除用户组 + * + * @param ids 用户组ids + */ + void delete(Set ids); + + /** + * 修改用户组成员 + * + * @param userGroupUpdDTO 新增组用户DTO实体 + */ + void updUserWithGroup(UserGroupUpdDTO userGroupUpdDTO); + + /** + * 删除用户组成员 + * + * @param userGroupDelDTO 删除用户组成员 + */ + void delUserWithGroup(UserGroupUpdDTO userGroupDelDTO); + + /** + * 获取没有归属组的用户 + * + * @return List 没有归属组的用户 + */ + List findUserWithOutGroup(); + + /** + * 获取用户组成员信息 + * + * @param groupId 用户组id + * @return List 用户列表 + */ + List queryUserByGroupId(Long groupId); + + /** + * 批量修改用户组成员的状态 + * + * @param userStateUpdateDTO 实体DTO + */ + void updateUserState(UserStateUpdateDTO userStateUpdateDTO); + + /** + * 批量删除用户组用户 + * + * @param userGroupUpdDTO 批量删除用户组用户DTO + */ + void delUser(UserGroupUpdDTO userGroupUpdDTO); + + /** + * 批量修改用户组用户的角色 + * + * @param userRoleUpdateDTO + */ + void updateUserRole(UserRoleUpdateDTO userRoleUpdateDTO); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserService.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserService.java new file mode 100644 index 0000000..b73b0f2 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/UserService.java @@ -0,0 +1,224 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.entity.User; +import org.dubhe.biz.base.dto.TeamDTO; +import org.dubhe.biz.base.dto.UserDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.cloud.authconfig.service.AdminUserService; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description Demo服务接口 + * @date 2020-11-26 + */ +public interface UserService extends AdminUserService, IService { + + + /** + * 根据ID获取用户信息 + * + * @param id 用户Id + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + UserDTO findById(long id); + + /** + * 新增用户 + * + * @param resources 用户新增实体 + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + UserDTO create(UserCreateDTO resources); + + /** + * 修改用户 + * + * @param resources 用户修改请求实例 + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + UserDTO update(UserUpdateDTO resources); + + /** + * 批量删除用户信息 + * + * @param ids 用户ID列表 + */ + void delete(Set ids); + + /** + * 根据用户名称获取用户信息 + * + * @param userName 用户名称 + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + UserDTO findByName(String userName); + + /** + * 修改用户密码 + * + * @param username 账号 + * @param encryptPassword 密码 + * @return void + */ + void updatePass(String username, String encryptPassword); + + /** + * 修改头像 + * + * @param realName 文件名 + * @param path 文件路径 + */ + void updateAvatar(String realName, String path); + + /** + * 修改邮箱 + * + * @param username 用户名 + * @param email 邮箱 + */ + void updateEmail(String username, String email); + + /** + * 分页查询用户列表 + * + * @param criteria 查询条件 + * @param page 分页请求实体 + * @return java.lang.Object 用户列表返回实例 + */ + Object queryAll(UserQueryDTO criteria, Page page); + + /** + * 查询用户列表 + * + * @param criteria 用户查询条件 + * @return java.util.List 用户列表返回实例 + */ + List queryAll(UserQueryDTO criteria); + + /** + * 根据用户ID查询团队列表 + * + * @param userId 用户ID + * @return java.util.List 团队列表信息 + */ + List queryTeams(Long userId); + + /** + * 导出数据 + * + * @param queryAll 待导出的数据 + * @param response 导出http响应 + * @throws IOException 导出异常 + */ + void download(List queryAll, HttpServletResponse response) throws IOException; + + /** + * 修改用户个人中心信息 + * + * @param resources 个人用户信息修改请求实例 + */ + void updateCenter(UserCenterUpdateDTO resources); + + /** + * 查询用户ID权限 + * + * @param id 用户ID + * @return java.util.Set 权限列表 + */ + Set queryPermissionByUserId(Long id); + + /** + * 用户注册信息 + * + * @param userRegisterDTO 用户注册请求实体 + * @return org.dubhe.base.DataResponseBody 注册返回结果集 + */ + DataResponseBody userRegister(UserRegisterDTO userRegisterDTO); + + + /** + * 获取code通过发送邮件 + * + * @param userRegisterMailDTO 用户发送邮件请求实体 + * @return org.dubhe.base.DataResponseBody 发送邮件返回结果集 + */ + DataResponseBody getCodeBySentEmail(UserRegisterMailDTO userRegisterMailDTO); + + + /** + * 邮箱修改 + * + * @param userEmailUpdateDTO 修改邮箱请求实体 + * @return org.dubhe.base.DataResponseBody 修改邮箱返回结果集 + */ + DataResponseBody resetEmail(UserEmailUpdateDTO userEmailUpdateDTO); + + /** + * 获取用户信息 + * + * @return java.util.Map 用户信息结果集 + */ + Map userinfo(); + + /** + * 密码重置接口 + * + * @param userResetPasswordDTO 密码修改请求参数 + * @return org.dubhe.base.DataResponseBody 密码修改结果集 + */ + DataResponseBody resetPassword(UserResetPasswordDTO userResetPasswordDTO); + + /** + * 登录 + * + * @param authUserDTO 登录请求实体 + */ + DataResponseBody> login(AuthUserDTO authUserDTO); + + /** + * 退出登录 + * + * @param accessToken token + */ + DataResponseBody logout(String accessToken); + + /** + * 根据用户昵称获取用户信息 + * + * @param nickName 用户昵称 + * @return org.dubhe.domain.dto.UserDTO 用户信息DTO集合 + */ + List findByNickName(String nickName); + + /** + * 根据用户id批量查询用户信息 + * + * @param ids 用户id集合 + * @return org.dubhe.domain.dto.UserDTO 用户信息DTO集合 + */ + List getUserList(List ids); +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictConvert.java new file mode 100644 index 0000000..9c3f52a --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictConvert.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.convert; + +import org.dubhe.admin.domain.dto.DictDTO; +import org.dubhe.admin.domain.entity.Dict; +import org.dubhe.biz.db.base.BaseConvert; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + + +/** + * @description 字典 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DictConvert extends BaseConvert { + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictDetailConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictDetailConvert.java new file mode 100644 index 0000000..41fe4ae --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictDetailConvert.java @@ -0,0 +1,31 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.convert; + +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.entity.DictDetail; +import org.dubhe.admin.domain.dto.DictDetailDTO; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 字典详情 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DictDetailConvert extends BaseConvert { +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictSmallConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictSmallConvert.java new file mode 100644 index 0000000..9a19560 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/DictSmallConvert.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.convert; + +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.dto.DictSmallQueryDTO; +import org.dubhe.admin.domain.entity.Dict; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 字典详情 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DictSmallConvert extends BaseConvert { + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/LogConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/LogConvert.java new file mode 100644 index 0000000..f88fcd3 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/LogConvert.java @@ -0,0 +1,32 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dubhe.admin.service.convert; + +import org.dubhe.admin.domain.dto.LogDTO; +import org.dubhe.admin.domain.entity.Log; +import org.dubhe.biz.db.base.BaseConvert; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 日志 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface LogConvert extends BaseConvert { + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/MenuConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/MenuConvert.java new file mode 100644 index 0000000..0f8e52a --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/MenuConvert.java @@ -0,0 +1,32 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service.convert; + + +import org.dubhe.admin.domain.dto.MenuDTO; +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.entity.Menu; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 菜单 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MenuConvert extends BaseConvert { + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/PermissionConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/PermissionConvert.java new file mode 100644 index 0000000..8a82aa9 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/PermissionConvert.java @@ -0,0 +1,31 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.convert; + +import org.dubhe.admin.domain.entity.Permission; +import org.dubhe.admin.domain.vo.PermissionVO; +import org.dubhe.biz.db.base.BaseConvert; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description permission转换器 + * @date 2021-05-31 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface PermissionConvert extends BaseConvert { +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleConvert.java new file mode 100644 index 0000000..d1112a2 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleConvert.java @@ -0,0 +1,30 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service.convert; + +import org.dubhe.admin.domain.dto.RoleDTO; +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.entity.Role; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 角色 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface RoleConvert extends BaseConvert { +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleSmallConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleSmallConvert.java new file mode 100644 index 0000000..3fd21ee --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/RoleSmallConvert.java @@ -0,0 +1,31 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service.convert; + +import org.dubhe.admin.domain.dto.RoleSmallDTO; +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.entity.Role; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 角色 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface RoleSmallConvert extends BaseConvert { + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamConvert.java new file mode 100644 index 0000000..767ea07 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamConvert.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service.convert; + +import org.dubhe.biz.base.dto.TeamDTO; +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.entity.Team; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 团队 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface TeamConvert extends BaseConvert { + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamSmallConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamSmallConvert.java new file mode 100644 index 0000000..ee2b2fe --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/TeamSmallConvert.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service.convert; + +import org.dubhe.biz.base.dto.TeamSmallDTO; +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.entity.Team; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 团队 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface TeamSmallConvert extends BaseConvert { +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/UserConvert.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/UserConvert.java new file mode 100644 index 0000000..ff77543 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/convert/UserConvert.java @@ -0,0 +1,31 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service.convert; + +import org.dubhe.biz.base.dto.UserDTO; +import org.dubhe.biz.db.base.BaseConvert; +import org.dubhe.admin.domain.entity.User; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @description 用户 转换类 + * @date 2020-06-01 + */ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface UserConvert extends BaseConvert { + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/AuthCodeServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/AuthCodeServiceImpl.java new file mode 100644 index 0000000..758d57b --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/AuthCodeServiceImpl.java @@ -0,0 +1,236 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dubhe.admin.dao.AuthCodeMapper; +import org.dubhe.admin.domain.dto.AuthCodeCreateDTO; +import org.dubhe.admin.domain.dto.AuthCodeQueryDTO; +import org.dubhe.admin.domain.dto.AuthCodeUpdateDTO; +import org.dubhe.admin.domain.dto.RoleAuthUpdateDTO; +import org.dubhe.admin.domain.entity.Auth; +import org.dubhe.admin.domain.entity.AuthPermission; +import org.dubhe.admin.domain.entity.Permission; +import org.dubhe.admin.domain.entity.RoleAuth; +import org.dubhe.admin.domain.vo.AuthVO; +import org.dubhe.admin.service.AuthCodeService; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.ReflectionUtils; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.db.utils.PageUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description 权限组服务实现类 + * @date 2021-05-14 + */ +@Service +public class AuthCodeServiceImpl extends ServiceImpl implements AuthCodeService { + + @Autowired + private AuthCodeMapper authCodeMapper; + + @Autowired + private UserContextService userContextService; + + public final static List FIELD_NAMES; + + static { + FIELD_NAMES = ReflectionUtils.getFieldNames(AuthVO.class); + } + + /** + * 分页查询权限组信息 + * + * @param authCodeQueryDTO 分页查询实例 + * @return Map 权限组分页信息 + */ + @Override + public Map queryAll(AuthCodeQueryDTO authCodeQueryDTO) { + + Page page = authCodeQueryDTO.toPage(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (StringUtils.isNotEmpty(authCodeQueryDTO.getAuthCode())) { + queryWrapper.and(x -> x.eq("id", authCodeQueryDTO.getAuthCode()).or().like("authCOde", authCodeQueryDTO.getAuthCode())); + } + + //排序 + IPage groupList; + try { + if (authCodeQueryDTO.getSort() != null && FIELD_NAMES.contains(authCodeQueryDTO.getSort())) { + if (StringConstant.SORT_ASC.equalsIgnoreCase(authCodeQueryDTO.getOrder())) { + queryWrapper.orderByAsc(StringUtils.humpToLine(authCodeQueryDTO.getSort())); + } else { + queryWrapper.orderByDesc(StringUtils.humpToLine(authCodeQueryDTO.getSort())); + } + } else { + queryWrapper.orderByDesc(StringConstant.ID); + } + groupList = authCodeMapper.selectPage(page, queryWrapper); + } catch (Exception e) { + throw new BusinessException("查询权限组列表展示异常"); + } + List authResult = groupList.getRecords().stream().map(x -> { + AuthVO authVO = new AuthVO(); + BeanUtils.copyProperties(x, authVO); + List permissions = authCodeMapper.getPermissionByAuthId(x.getId()); + authVO.setPermissions(permissions); + return authVO; + }).collect(Collectors.toList()); + return PageUtil.toPage(page, authResult); + } + + /** + * 创建权限组 + * + * @param authCodeCreateDTO 创建权限组DTO实例 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void create(AuthCodeCreateDTO authCodeCreateDTO) { + //获取当前用户id + Long curUserId = userContextService.getCurUserId(); + Auth auth = new Auth(); + BeanUtil.copyProperties(authCodeCreateDTO, auth); + checkAuthCodeIsExist(auth); + auth.setCreateUserId(curUserId); + authCodeMapper.insert(auth); + tiedWithPermission(auth.getId(), authCodeCreateDTO.getPermissions()); + } + + /** + * 修改权限组信息 + * + * @param authCodeUpdateDTO 修改权限组信息DTO实例 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void update(AuthCodeUpdateDTO authCodeUpdateDTO) { + //获取当前用户id + Long curUserId = userContextService.getCurUserId(); + Auth auth = new Auth(); + BeanUtil.copyProperties(authCodeUpdateDTO, auth); + checkAuthCodeIsExist(auth); + auth.setUpdateUserId(curUserId); + authCodeMapper.updateById(auth); + if (CollUtil.isNotEmpty(authCodeUpdateDTO.getPermissions())) { + authCodeMapper.untiedWithPermission(authCodeUpdateDTO.getId()); + tiedWithPermission(auth.getId(), authCodeUpdateDTO.getPermissions()); + } + } + + /** + * 批量删除权限组 + * + * @param ids 权限组id集合 + */ + @Override + public void delete(Set ids) { + //删除权限组数据 + removeByIds(ids); + //清除权限组和权限映射关系 + for (Long id : ids) { + authCodeMapper.untiedWithPermission(id); + } + } + + + private void tiedWithPermission(Long authId, Set permissionIds) { + List list = new ArrayList<>(); + for (Long id : permissionIds) { + AuthPermission authPermission = new AuthPermission(); + authPermission.setAuthId(authId); + authPermission.setPermissionId(id); + list.add(authPermission); + } + authCodeMapper.tiedWithPermission(list); + } + + /** + * 修改角色权限 + * + * @param roleAuthUpdateDTO + * @return + */ + @Override + public void updateRoleAuth(RoleAuthUpdateDTO roleAuthUpdateDTO) { + authCodeMapper.untiedRoleAuthByRoleId(roleAuthUpdateDTO.getRoleId()); + List roleAuths = new ArrayList<>(); + if(CollUtil.isNotEmpty(roleAuthUpdateDTO.getAuthIds())) { + for (Long authId : roleAuthUpdateDTO.getAuthIds()) { + RoleAuth roleAuth = new RoleAuth(); + roleAuth.setRoleId(roleAuthUpdateDTO.getRoleId()); + roleAuth.setAuthId(authId); + roleAuths.add(roleAuth); + } + authCodeMapper.tiedRoleAuth(roleAuths); + } + } + + /** + * 获取权限组tree + * + * @return List 权限组tree + */ + @Override + public List getAuthCodeList() { + List authList = authCodeMapper.selectList(new LambdaQueryWrapper<>()); + List resultList = new ArrayList<>(); + if (CollUtil.isNotEmpty(authList)) { + for (Auth auth : authList) { + AuthVO authVO = new AuthVO(); + BeanUtils.copyProperties(auth, authVO); + List permissions = authCodeMapper.getPermissionByAuthId(authVO.getId()); + authVO.setPermissions(permissions); + resultList.add(authVO); + } + } + return resultList; + } + + /** + * 根据authCode获取权限组 + * + * @param auth 权限组名称 + * @return List 权限组列表 + */ + private void checkAuthCodeIsExist(Auth auth) { + List authList = authCodeMapper.selectList(new LambdaQueryWrapper() + .eq(Auth::getAuthCode, auth.getAuthCode())); + for (Auth authObj : authList) { + if (Objects.equals(auth.getAuthCode(), authObj.getAuthCode()) && + !Objects.equals(auth.getId(), authObj.getId())) { + throw new BusinessException("权限组名称不能重复"); + } + } + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DataSequenceServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DataSequenceServiceImpl.java new file mode 100644 index 0000000..2f33451 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DataSequenceServiceImpl.java @@ -0,0 +1,102 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service.impl; + +import org.dubhe.admin.dao.DataSequenceMapper; +import org.dubhe.admin.domain.entity.DataSequence; +import org.dubhe.biz.base.exception.DataSequenceException; +import org.dubhe.admin.service.DataSequenceService; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +/** + * @description 获取序列服务接口实现 + * @date 2020-09-23 + */ +@Service +public class DataSequenceServiceImpl implements DataSequenceService { + + @Autowired + private DataSequenceMapper dataSequenceMapper; + + @Override + public DataSequence getSequence(String businessCode) { + return dataSequenceMapper.selectByBusiness(businessCode); + } + + /** + * 修改步长 + * + * @param businessCode 业务编码 + * @return + */ + @Override + public int updateSequenceStart(String businessCode) { + return dataSequenceMapper.updateStartByBusinessCode(businessCode); + } + + /** + * 检查表是否存在 + * + * @param tableName 表名 + * @return 是否存在标识 + */ + @Override + public boolean checkTableExist(String tableName) { + try { + dataSequenceMapper.checkTableExist(tableName); + return true; + }catch (Exception e){ + LogUtil.info(LogEnum.DATA_SEQUENCE,"表不存在"); + return false; + } + } + + /** + * 创建表 + * + * @param tableName 表名 + */ + @Override + public void createTable(String tableName) { + String oldTableName = tableName.substring(0,tableName.lastIndexOf("_")); + dataSequenceMapper.createNewTable(tableName,oldTableName); + } + + /** + * 扩容可用数量 + * + * @param businessCode 业务编码 + * @return DataSequence 数据ID序列 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public DataSequence expansionUsedNumber(String businessCode) { + DataSequence dataSequenceNew = getSequence(businessCode); + if (dataSequenceNew == null || dataSequenceNew.getStart() == null || dataSequenceNew.getStep() == null) { + throw new DataSequenceException("配置出错,请检查data_sequence表配置"); + } + updateSequenceStart(businessCode); + return dataSequenceNew; + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictDetailServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictDetailServiceImpl.java new file mode 100644 index 0000000..0683e73 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictDetailServiceImpl.java @@ -0,0 +1,168 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.dao.DictDetailMapper; +import org.dubhe.admin.domain.dto.DictDetailCreateDTO; +import org.dubhe.admin.domain.dto.DictDetailDTO; +import org.dubhe.admin.domain.dto.DictDetailQueryDTO; +import org.dubhe.admin.domain.dto.DictDetailUpdateDTO; +import org.dubhe.admin.domain.entity.DictDetail; +import org.dubhe.admin.service.DictDetailService; +import org.dubhe.admin.service.convert.DictDetailConvert; +import org.dubhe.biz.base.dto.DictDetailQueryByLabelNameDTO; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.vo.DictDetailVO; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @description 字典详情服务 实现类 + * @date 2020-06-01 + */ +@Service +public class DictDetailServiceImpl implements DictDetailService { + + @Autowired + private DictDetailMapper dictDetailMapper; + + @Autowired + private DictDetailConvert dictDetailConvert; + + /** + * 分页查询字典详情 + * + * @param criteria 字典详情查询实体 + * @param page 分页实体 + * @return java.util.Map 字典详情分页实例 + */ + @Override + public Map queryAll(DictDetailQueryDTO criteria, Page page) { + IPage dictDetails = dictDetailMapper.selectPage(page, WrapperHelp.getWrapper(criteria)); + return PageUtil.toPage(dictDetails, dictDetailConvert::toDto); + } + + + /** + * 按条件查询字典列表 + * + * @param criteria 字典详情查询实体 + * @return java.util.List 字典详情实例 + */ + @Override + public List queryAll(DictDetailQueryDTO criteria) { + List list = dictDetailMapper.selectList(WrapperHelp.getWrapper(criteria)); + return dictDetailConvert.toDto(list); + } + + + /** + * 根据ID查询字典详情 + * + * @param id 字典详情ID + * @return org.dubhe.domain.dto.DictDetailDTO 字典详情实例 + */ + @Override + public DictDetailDTO findById(Long id) { + DictDetail dictDetail = dictDetailMapper.selectById(id); + return dictDetailConvert.toDto(dictDetail); + } + + /** + * 新增字典性情 + * + * @param resources 字典详情新增实体 + * @return org.dubhe.domain.dto.DictDetailDTO 字典详情实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public DictDetailDTO create(DictDetailCreateDTO resources) { + if (!ObjectUtil.isNull(dictDetailMapper.selectByDictIdAndLabel(resources.getDictId(), resources.getLabel()))) { + throw new BusinessException("字典标签已存在"); + } + DictDetail dictDetail = DictDetail.builder().build(); + BeanUtils.copyProperties(resources, dictDetail); + dictDetailMapper.insert(dictDetail); + return dictDetailConvert.toDto(dictDetail); + } + + /** + * 修改字典详情 + * + * @param resources 字典详情修改实体 + * @return void + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(DictDetailUpdateDTO resources) { + DictDetail detail = dictDetailMapper.selectByDictIdAndLabel(resources.getDictId(), resources.getLabel()); + if (detail != null && !detail.getId().equals(resources.getId())) { + throw new BusinessException("字典标签已存在"); + } + DictDetail dbDetail = DictDetail.builder().build(); + BeanUtils.copyProperties(resources, dbDetail); + dictDetailMapper.updateById(dbDetail); + } + + /** + * 删除字典详情 + * + * @param ids 字典详情ID + * @return void + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + dictDetailMapper.deleteBatchIds(ids); + } + + /** + * + * @param dictDetailQueryByLabelNameDTO 字典名称 + * @return List 字典集合 + */ + @Override + public List getDictName(DictDetailQueryByLabelNameDTO dictDetailQueryByLabelNameDTO) { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(DictDetail::getLabel, dictDetailQueryByLabelNameDTO.getName()); + List dictDetails = dictDetailMapper.selectList(lambdaQueryWrapper); + List DictDetailVOS = null; + if (!CollectionUtils.isEmpty(dictDetails)) { + DictDetailVOS = dictDetails.stream().map(x -> { + DictDetailVO dictDetailVO = new DictDetailVO(); + BeanUtils.copyProperties(x, dictDetailVO); + return dictDetailVO; + }).collect(Collectors.toList()); + } + return DictDetailVOS; + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictServiceImpl.java new file mode 100644 index 0000000..55b387c --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/DictServiceImpl.java @@ -0,0 +1,208 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.dao.DictDetailMapper; +import org.dubhe.admin.dao.DictMapper; +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.entity.Dict; +import org.dubhe.admin.service.DictService; +import org.dubhe.admin.service.convert.DictConvert; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.file.utils.DubheFileUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * @description 字典服务 实现类 + * @date 2020-06-01 + */ +@Service +public class DictServiceImpl implements DictService { + + @Autowired + private DictMapper dictMapper; + + @Autowired + private DictDetailMapper dictDetailMapper; + + @Autowired + private DictConvert dictConvert; + + + /** + * 分页查询字典信息 + * + * @param criteria 字典查询实体 + * @param page 分页实体 + * @return java.util.Map 字典分页实例 + */ + @Override + public Map queryAll(DictQueryDTO criteria, Page page) { + IPage dicts = dictMapper.selectCollPage(page, WrapperHelp.getWrapper(criteria)); + return PageUtil.toPage(dicts, dictConvert::toDto); + } + + + /** + * 按条件查询字典列表 + * + * @param criteria 字典查询实体 + * @return java.util.List 字典实例 + */ + @Override + public List queryAll(DictQueryDTO criteria) { + List list = dictMapper.selectCollList(WrapperHelp.getWrapper(criteria)); + return dictConvert.toDto(list); + } + + /** + * 通过ID查询字典详情 + * + * @param id 字典ID + * @return org.dubhe.domain.dto.DictDTO 字典实例 + */ + @Override + public DictDTO findById(Long id) { + Dict dict = dictMapper.selectCollById(id); + return dictConvert.toDto(dict); + } + + /** + * 通过Name查询字典详情 + * + * @param name 字典名称 + * @return org.dubhe.domain.dto.DictDTO 字典实例 + */ + @Override + public DictDTO findByName(String name) { + Dict dict = dictMapper.selectCollByName(name); + return dictConvert.toDto(dict); + } + + /** + * 新增字典 + * + * @param resources 字典新增实体 + * @return org.dubhe.domain.dto.DictDTO 字典实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public DictDTO create(DictCreateDTO resources) { + if (dictMapper.selectCollByName(resources.getName()) != null) { + throw new BusinessException("字典名称已存在"); + } + Dict dict = Dict.builder().build(); + BeanUtils.copyProperties(resources, dict); + dictMapper.insert(dict); + // 级联保存子表 + resources.getDictDetails().forEach(detail -> { + detail.setDictId(dict.getId()); + dictDetailMapper.insert(detail); + }); + return dictConvert.toDto(dict); + } + + /** + * 字典修改 + * + * @param resources 字典修改实体 + * @return void + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(DictUpdateDTO resources) { + Dict dict = dictMapper.selectCollByName(resources.getName()); + if (dict != null && !dict.getId().equals(resources.getId())) { + throw new BusinessException("字典名称已存在"); + } + Dict dbDict = Dict.builder().build(); + BeanUtils.copyProperties(resources, dbDict); + dictMapper.updateById(dbDict); + //级联保存子表 + resources.getDictDetails().forEach(detail -> { + detail.setDictId(dbDict.getId()); + if (detail.getId() == null) { + dictDetailMapper.insert(detail); + } else { + dictDetailMapper.updateById(detail); + } + + }); + } + + + /** + * 字典批量删除 + * + * @param ids 字典ID + * @return void + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAll(Set ids) { + for (Long id : ids) { + dictMapper.deleteById(id); + dictDetailMapper.deleteByDictId(id); + } + } + + /** + * 字典导出 + * + * @param dictDtos 字典导出实体 + * @param response + * @return void + */ + @Override + public void download(List dictDtos, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (DictDTO dictDTO : dictDtos) { + if (CollectionUtil.isNotEmpty(dictDTO.getDictDetails())) { + for (DictDetailDTO dictDetail : dictDTO.getDictDetails()) { + Map map = new LinkedHashMap<>(); + map.put("字典名称", dictDTO.getName()); + map.put("字典描述", dictDTO.getRemark()); + map.put("字典标签", dictDetail.getLabel()); + map.put("字典值", dictDetail.getValue()); + map.put("创建日期", dictDetail.getCreateTime()); + list.add(map); + } + } else { + Map map = new LinkedHashMap<>(); + map.put("字典名称", dictDTO.getName()); + map.put("字典描述", dictDTO.getRemark()); + map.put("字典标签", null); + map.put("字典值", null); + map.put("创建日期", dictDTO.getCreateTime()); + list.add(map); + } + } + DubheFileUtil.downloadExcel(list, response); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/LogServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/LogServiceImpl.java new file mode 100644 index 0000000..9ac6125 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/LogServiceImpl.java @@ -0,0 +1,148 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.dao.LogMapper; +import org.dubhe.admin.domain.dto.LogQueryDTO; +import org.dubhe.admin.domain.entity.Log; +import org.dubhe.admin.service.LogService; +import org.dubhe.admin.service.convert.LogConvert; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.file.utils.DubheFileUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @description 日志 实现类 + * @date 2020-06-01 + */ +@Service +public class LogServiceImpl implements LogService { + + @Autowired + private LogConvert logConvert; + + @Autowired + private LogMapper logMapper; + + /** + * 分页查询日志 + * + * @param criteria 日志查询实体 + * @param page 分页实体 + * @return java.lang.Object 日志返回实例 + */ + @Override + public Object queryAll(LogQueryDTO criteria, Page page) { + IPage dicts = logMapper.selectPage(page, WrapperHelp.getWrapper(criteria)); + String status = "ERROR"; + if (status.equals(criteria.getLogType())) { + return PageUtil.toPage(dicts, logConvert::toDto); + } + return page; + } + + /** + * 查询日志列表 + * + * @param criteria 日志查询条件 + * @return java.util.List 日志返回实例 + */ + @Override + public List queryAll(LogQueryDTO criteria) { + return logMapper.selectList(WrapperHelp.getWrapper(criteria)); + } + + /** + * 分页查询日志 + * + * @param criteria 日志查询实体 + * @param page 分页实体 + * @return java.lang.Object 日志返回实例 + */ + @Override + public Object queryAllByUser(LogQueryDTO criteria, Page page) { + Page logs = logMapper.selectPage(page, WrapperHelp.getWrapper(criteria)); + return PageUtil.toPage(logs, logConvert::toDto); + } + + /** + * 根据id查询错误日志 + * + * @param id + * @return java.lang.Object + */ + @Override + public Object findByErrDetail(Long id) { + Log log = logMapper.selectById(id); + byte[] details = log.getExceptionDetail(); + return Dict.create().set("exception", new String(ObjectUtil.isNotNull(details) ? details : "".getBytes())); + } + + /** + * 日志信息导出 + * + * @param logs 日志导出列表 + * @param response + */ + @Override + public void download(List logs, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (Log log : logs) { + Map map = new LinkedHashMap<>(); + map.put("用户名", log.getUsername()); + map.put("IP", log.getRequestIp()); + map.put("描述", log.getDescription()); + map.put("浏览器", log.getBrowser()); + map.put("请求耗时/毫秒", log.getTime()); + map.put("异常详情", new String(ObjectUtil.isNotNull(log.getExceptionDetail()) ? log.getExceptionDetail() : "".getBytes())); + map.put("创建日期", log.getCreateTime()); + list.add(map); + } + DubheFileUtil.downloadExcel(list, response); + } + + /** + * 删除所有error日志 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delAllByError() { + logMapper.deleteByLogType("ERROR"); + } + + /** + * 删除所有info日志 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delAllByInfo() { + logMapper.deleteByLogType("INFO"); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MailServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MailServiceImpl.java new file mode 100644 index 0000000..541408e --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MailServiceImpl.java @@ -0,0 +1,150 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service.impl; + +import org.dubhe.admin.service.MailService; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.File; + +/** + * @description 邮件服务 实现类 + * @date 2020-06-01 + */ +@Service +public class MailServiceImpl implements MailService { + + + @Value("${spring.mail.username}") + private String from; + + @Resource + private JavaMailSender mailSender; + + /** + * 简单文本邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param contnet 邮件内容 + */ + @Override + public void sendSimpleMail(String to, String subject, String contnet) { + + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject(subject); + message.setText(contnet); + message.setFrom(from); + + mailSender.send(message); + } + + /** + * HTML 文本邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param contnet HTML内容 + * @throws MessagingException + */ + @Override + public void sendHtmlMail(String to, String subject, String contnet) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(contnet, true); + helper.setFrom(from); + + mailSender.send(message); + } + + + /** + * 附件邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param contnet HTML内容 + * @param filePath 附件路径 + * @throws MessagingException + */ + @Override + public void sendAttachmentsMail(String to, String subject, String contnet, + String filePath) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(contnet, true); + helper.setFrom(from); + + FileSystemResource file = new FileSystemResource(new File(filePath)); + String fileName = file.getFilename(); + helper.addAttachment(fileName, file); + + mailSender.send(message); + } + + /** + * 图片邮件 + * + * @param to 接收者邮件 + * @param subject 邮件主题 + * @param content HTML内容 + * @param rscPath 图片路径 + * @param rscId 图片ID + */ + @Override + public void sendLinkResourceMail(String to, String subject, String content, + String rscPath, String rscId) { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = null; + + try { + + helper = new MimeMessageHelper(message, true); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(content, true); + helper.setFrom(from); + + FileSystemResource res = new FileSystemResource(new File(rscPath)); + helper.addInline(rscId, res); + mailSender.send(message); + + } catch (MessagingException e) { + LogUtil.info(LogEnum.BIZ_SYS,"发送静态邮件失败: {}", e); + } + + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MenuServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MenuServiceImpl.java new file mode 100644 index 0000000..908ee3a --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/MenuServiceImpl.java @@ -0,0 +1,437 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.dubhe.admin.dao.MenuMapper; +import org.dubhe.admin.domain.dto.ExtConfigDTO; +import org.dubhe.admin.domain.dto.MenuCreateDTO; +import org.dubhe.admin.domain.dto.MenuDTO; +import org.dubhe.admin.domain.dto.MenuQueryDTO; +import org.dubhe.admin.domain.dto.MenuUpdateDTO; +import org.dubhe.admin.domain.dto.RoleSmallDTO; +import org.dubhe.admin.domain.entity.Menu; +import org.dubhe.admin.domain.vo.MenuMetaVo; +import org.dubhe.admin.domain.vo.MenuVo; +import org.dubhe.admin.enums.MenuTypeEnum; +import org.dubhe.admin.service.MenuService; +import org.dubhe.admin.service.RoleService; +import org.dubhe.admin.service.convert.MenuConvert; +import org.dubhe.biz.base.enums.SwitchEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.db.constant.PermissionConstant; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.file.utils.DubheFileUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @description 菜单服务 实现类 + * @date 2020-06-01 + */ +@Service +public class MenuServiceImpl implements MenuService { + + @Autowired + private MenuMapper menuMapper; + + @Autowired + private MenuConvert menuConvert; + + @Autowired + private RoleService roleService; + + /** + * 按条件查询菜单列表 + * + * @param criteria 菜单请求实体 + * @return java.util.List 菜单返回实例 + */ + @Override + public List queryAll(MenuQueryDTO criteria) { + List menus = menuMapper.selectList(WrapperHelp.getWrapper(criteria)); + return menuConvert.toDto(menus); + } + + + /** + * 根据id查询菜单信息 + * + * @param id 菜单id + * @return org.dubhe.domain.dto.MenuDTO 菜单返回实例 + */ + @Override + public MenuDTO findById(long id) { + + Menu menu = menuMapper.selectOne( + new LambdaQueryWrapper().eq(Menu::getId, id).eq(Menu::getDeleted, + SwitchEnum.getBooleanValue(SwitchEnum.OFF.getValue())) + ); + return menuConvert.toDto(menu); + } + + /** + * 根据角色查询菜单列表 + * + * @param roles 角色 + * @return java.util.List 菜单返回实例 + */ + @Override + public List findByRoles(List roles) { + Set roleIds = roles.stream().map(RoleSmallDTO::getId).collect(Collectors.toSet()); + List menus = menuMapper.findByRolesIdInAndTypeNotOrderBySortAsc(roleIds, 2); + return menus.stream().map(menuConvert::toDto).collect(Collectors.toList()); + } + + /** + * 新增菜单 + * + * @param resources 菜单新增请求实体 + * @return org.dubhe.domain.dto.MenuDTO 菜单返回实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public MenuDTO create(MenuCreateDTO resources) { + if (StringUtils.isNotBlank(resources.getComponentName())) { + if (menuMapper.findByComponentName(resources.getComponentName()) != null) { + throw new BusinessException("路由名称已存在"); + } + } + if (MenuTypeEnum.LINK_TYPE.getValue().equals(resources.getType())) { + String http = "http://", https = "https://"; + if (!(resources.getPath().toLowerCase().startsWith(http) || resources.getPath().toLowerCase().startsWith(https))) { + throw new BusinessException("外链必须以http://或者https://开头"); + } + } + Menu menu = Menu.builder() + .component(resources.getComponent()) + .cache(resources.getCache()) + .componentName(resources.getComponentName()) + .hidden(resources.getHidden()) + .layout(resources.getLayout()) + .icon(resources.getIcon()) + .name(resources.getName()) + .path(resources.getPath()) + .pid(resources.getPid()) + .permission(resources.getPermission()) + .sort(resources.getSort()) + .type(resources.getType()) + .build(); + if(MenuTypeEnum.PAGE_TYPE.getValue().equals(resources.getType())){ + menu.setBackTo(resources.getBackTo()); + menu.setExtConfig(resources.getExtConfig()); + } + menuMapper.insert(menu); + //管理员新增默认权限 + roleService.tiedRoleMenu(PermissionConstant.ADMIN_ROLE_ID,menu.getId()); + + return menuConvert.toDto(menu); + } + + /** + * 修改菜单 + * + * @param resources 菜单修改请求实体 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(MenuUpdateDTO resources) { + if (resources.getId().equals(resources.getPid())) { + throw new BusinessException("上级不能为自己"); + } + Menu menu = menuMapper.selectOne( + new LambdaQueryWrapper() + .eq(Menu::getId, resources.getId()) + .eq(Menu::getDeleted, SwitchEnum.getBooleanValue(SwitchEnum.OFF.getValue())) + ); + if (MenuTypeEnum.LINK_TYPE.getValue().equals(resources.getType())) { + String http = "http://", https = "https://"; + if (!(resources.getPath().toLowerCase().startsWith(http) || resources.getPath().toLowerCase().startsWith(https))) { + throw new BusinessException("外链必须以http://或者https://开头"); + } + } + + if (StringUtils.isNotBlank(resources.getComponentName())) { + Menu dbMenu = menuMapper.findByComponentName(resources.getComponentName()); + if (dbMenu != null && !dbMenu.getId().equals(menu.getId())) { + throw new BusinessException("路由名称已存在"); + } + } + menu.setName(resources.getName()); + menu.setComponent(resources.getComponent()); + menu.setPath(resources.getPath()); + menu.setIcon(resources.getIcon()); + menu.setType(resources.getType()); + menu.setLayout(resources.getLayout()); + menu.setPid(resources.getPid()); + menu.setSort(resources.getSort()); + menu.setCache(resources.getCache()); + menu.setHidden(resources.getHidden()); + menu.setComponentName(resources.getComponentName()); + menu.setPermission(resources.getPermission()); + if(MenuTypeEnum.PAGE_TYPE.getValue().equals(resources.getType())){ + ExtConfigDTO extConfigDTO = analyzeBackToValue(resources.getExtConfig()); + menu.setBackTo(Objects.isNull(extConfigDTO)?null:extConfigDTO.getBackTo()); + menu.setExtConfig(resources.getExtConfig()); + } + menuMapper.updateById(menu); + } + + + /** + * 解析扩展配置中 backTO 属性值 + * + * @param extConfig 扩展配置 + * @return ExtConfigDTO扩展配置 + */ + private ExtConfigDTO analyzeBackToValue(String extConfig){ + ExtConfigDTO dto = ExtConfigDTO.builder().build(); + try { + if(!Objects.isNull(extConfig)){ + dto = JSONObject.parseObject(extConfig, ExtConfigDTO.class); + } + }catch (Exception e){ + LogUtil.error(LogEnum.SYS_ERR,"analyzeBackToValue error, params:{} , error:{}",JSONObject.toJSONString(extConfig),e); + } + return dto; + } + + /** + * 查询可删除的菜单 + * + * @param menuList + * @param menuSet + * @return java.util.Set + */ + @Override + public Set getDeleteMenus(List menuList, Set menuSet) { + // 递归找出待删除的菜单 + for (Menu menu1 : menuList) { + menuSet.add(menu1); + List menus = menuMapper.findByPid(menu1.getId()); + if (menus != null && menus.size() != 0) { + getDeleteMenus(menus, menuSet); + } + } + return menuSet; + } + + /** + * 删除菜单 + * + * @param menuSet 删除菜单请求集合 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set menuSet) { + for (Menu menu : menuSet) { + roleService.untiedMenu(menu.getId()); + menuMapper.updateById( + Menu.builder() + .id(menu.getId()) + .deleted(SwitchEnum.getBooleanValue(SwitchEnum.ON.getValue())).build() + ); + } + } + + /** + * 获取菜单树 + * + * @param menus 菜单列表 + * @return java.lang.Object 菜单树结构列表 + */ + @Override + public Object getMenuTree(List menus) { + List> list = new LinkedList<>(); + menus.forEach(menu -> { + if (menu != null) { + List menuList = menuMapper.findByPid(menu.getId()); + Map map = new HashMap<>(16); + map.put("id", menu.getId()); + map.put("label", menu.getName()); + if (menuList != null && menuList.size() != 0) { + map.put("children", getMenuTree(menuList)); + } + list.add(map); + } + } + ); + return list; + } + + /** + * 根据ID获取菜单列表 + * + * @param pid id + * @return java.util.List 菜单返回列表 + */ + @Override + public List findByPid(long pid) { + return menuMapper.findByPid(pid); + } + + + /** + * 构建菜单树 + * + * @param menuDtos 菜单请求实体 + * @return java.util.Map 菜单树结构 + */ + @Override + public Map buildTree(List menuDtos) { + List trees = new ArrayList<>(); + Set ids = new HashSet<>(); + for (MenuDTO menuDTO : menuDtos) { + if (menuDTO.getPid() == 0) { + trees.add(menuDTO); + } + for (MenuDTO it : menuDtos) { + if (it.getPid().equals(menuDTO.getId())) { + if (menuDTO.getChildren() == null) { + menuDTO.setChildren(new ArrayList<>()); + } + menuDTO.getChildren().add(it); + ids.add(it.getId()); + } + } + } + Map map = new HashMap<>(2); + if (trees.size() == 0) { + trees = menuDtos.stream().filter(s -> !ids.contains(s.getId())).collect(Collectors.toList()); + } + // MenuTree 不分页,结构保持一致 + Map page = new HashMap<>(2); + page.put("current", 1); + page.put("size", menuDtos.size()); + page.put("total", menuDtos.size()); + + map.put("result", trees); + map.put("page", page); + + return map; + } + + + /** + * 构建菜单树 + * + * @param menuDtos 菜单请求实体 + * @return java.util.List 菜单树返回实例 + */ + @Override + public List buildMenus(List menuDtos) { + List list = new LinkedList<>(); + menuDtos.forEach(menuDTO -> { + if (menuDTO != null) { + List menuDtoList = menuDTO.getChildren(); + MenuVo menuVo = new MenuVo(); + menuVo.setName(ObjectUtil.isNotEmpty(menuDTO.getComponentName()) ? menuDTO.getComponentName() : menuDTO.getName()); + // 一级目录需要加斜杠,不然会报警告 + menuVo.setPath(menuDTO.getPid() == 0 ? "/" + menuDTO.getPath() : menuDTO.getPath()); + menuVo.setHidden(menuDTO.getHidden()); + // 如果不是外链 + if (MenuTypeEnum.LINK_TYPE.getValue().compareTo(menuDTO.getType()) != 0) { + if (menuDTO.getPid() == 0) { + menuVo.setComponent(StrUtil.isEmpty(menuDTO.getComponent()) ? "Layout" : menuDTO.getComponent()); + } else if (!StrUtil.isEmpty(menuDTO.getComponent())) { + menuVo.setComponent(menuDTO.getComponent()); + } + } + menuVo.setMeta(new MenuMetaVo(menuDTO.getName(), menuDTO.getIcon(), menuDTO.getLayout(), !menuDTO.getCache())); + if (menuDtoList != null && menuDtoList.size() != 0) { + menuVo.setChildren(buildMenus(menuDtoList)); + // 处理是一级菜单并且没有子菜单的情况 + } else if (menuDTO.getPid() == 0) { + MenuVo menuVo1 = new MenuVo(); + menuVo1.setMeta(menuVo.getMeta()); + // 非外链 + if (MenuTypeEnum.LINK_TYPE.getValue().compareTo(menuDTO.getType()) != 0) { + menuVo1.setPath(menuVo.getPath()); + menuVo1.setName(menuVo.getName()); + menuVo1.setComponent(menuVo.getComponent()); + } else { + menuVo1.setPath(menuDTO.getPath()); + } + menuVo.setName(null); + menuVo.setMeta(null); + menuVo.setComponent("Layout"); + List list1 = new ArrayList<>(); + list1.add(menuVo1); + menuVo.setChildren(list1); + } + list.add(menuVo); + } + } + ); + return list; + } + + + /** + * 获取菜单 + * + * @param id 菜单id + * @return org.dubhe.domain.entity.Menu 菜单返回实例 + */ + @Override + public Menu findOne(Long id) { + Menu menu = menuMapper.selectById(id); + return menu; + } + + + /** + * 导出菜单 + * + * @param menuDtos 菜单列表 + * @param response + */ + @Override + public void download(List menuDtos, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (MenuDTO menuDTO : menuDtos) { + Map map = new LinkedHashMap<>(); + map.put("菜单名称", menuDTO.getName()); + map.put("菜单类型", MenuTypeEnum.getEnumValue(menuDTO.getType()).getDesc()); + map.put("权限标识", menuDTO.getPermission()); + map.put("菜单可见", menuDTO.getHidden() ? "否" : "是"); + map.put("是否缓存", menuDTO.getCache() ? "是" : "否"); + map.put("创建日期", menuDTO.getCreateTime()); + list.add(map); + } + DubheFileUtil.downloadExcel(list, response); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/PermissionServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/PermissionServiceImpl.java new file mode 100644 index 0000000..a1bb2e0 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/PermissionServiceImpl.java @@ -0,0 +1,216 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dubhe.admin.dao.AuthCodeMapper; +import org.dubhe.admin.dao.PermissionMapper; +import org.dubhe.admin.domain.dto.PermissionCreateDTO; +import org.dubhe.admin.domain.dto.PermissionDeleteDTO; +import org.dubhe.admin.domain.dto.PermissionQueryDTO; +import org.dubhe.admin.domain.dto.PermissionUpdateDTO; +import org.dubhe.admin.domain.entity.Permission; +import org.dubhe.admin.domain.vo.PermissionVO; +import org.dubhe.admin.service.PermissionService; +import org.dubhe.admin.service.convert.PermissionConvert; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description 操作权限服务实现类 + * @date 2021-04-28 + */ +@Service +public class PermissionServiceImpl extends ServiceImpl implements PermissionService { + + @Autowired + private PermissionMapper permissionMapper; + + @Autowired + private UserContextService userContextService; + + @Autowired + private PermissionConvert permissionConvert; + + @Autowired + private AuthCodeMapper authCodeMapper; + + /** + * + * 获取权限列表 + * @param pid 权限父id + * @return java.util.List 权限列表 + */ + @Override + public List findByPid(long pid) { + return permissionMapper.findByPid(pid); + } + + /** + * 获取权限树 + * + * @param permissions 权限列表 + * @return Object 权限树列表结构 + */ + @Override + public Object getPermissionTree(List permissions) { + List> list = new LinkedList<>(); + permissions.forEach(permission -> { + if (permission != null) { + List authList = permissionMapper.findByPid(permission.getId()); + Map map = new HashMap<>(16); + map.put("id", permission.getId()); + map.put("permission", permission.getPermission()); + map.put("label", permission.getName()); + if (CollUtil.isNotEmpty(authList)) { + map.put("children", getPermissionTree(authList)); + } + list.add(map); + } + } + ); + return list; + } + + /** + * 获取权限列表 + * + * @param permissionQueryDTO 权限查询DTO + * @return Map + */ + @Override + public Map queryAll(PermissionQueryDTO permissionQueryDTO) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotEmpty(permissionQueryDTO.getKeyword())) { + queryWrapper.and(x -> x.like(Permission::getName, permissionQueryDTO.getKeyword()).or().like(Permission::getPermission, permissionQueryDTO.getKeyword())); + } + queryWrapper.orderByDesc(Permission::getCreateTime); + List permissions = permissionMapper.selectList(queryWrapper); + return buildTree(permissionConvert.toDto(permissions)); + } + + private Map buildTree(List permissions) { + List trees = new ArrayList<>(); + Set ids = new HashSet<>(); + for (PermissionVO permissionVO : permissions) { + if (permissionVO.getPid() == 0) { + trees.add(permissionVO); + } + for (PermissionVO vo : permissions) { + if (vo.getPid().equals(permissionVO.getId())) { + if (CollUtil.isEmpty(permissionVO.getChildren())) { + permissionVO.setChildren(new ArrayList<>()); + } + permissionVO.getChildren().add(vo); + ids.add(vo.getId()); + } + } + + } + + Map map = new HashMap<>(2); + if (trees.size() == 0) { + permissions.stream().filter(x -> !ids.contains(x.getId())).collect(Collectors.toList()); + } + + Map page = new HashMap<>(3); + page.put("current", 1); + page.put("size", permissions.size()); + page.put("total", permissions.size()); + + map.put("result", trees); + map.put("page", page); + return map; + } + + /** + * 新增权限 + * + * @param permissionCreateDTO 新增权限DTO + */ + @Override + public void create(PermissionCreateDTO permissionCreateDTO) { + UserContext curUser = userContextService.getCurUser(); + List permissions = new ArrayList<>(); + for (Permission resource : permissionCreateDTO.getPermissions()) { + if (permissionMapper.findByName(resource.getName()) != null) { + throw new BusinessException("权限名称已存在"); + } + + Permission permission = new Permission(); + permission.setPid(permissionCreateDTO.getPid()) + .setName(resource.getName()) + .setPermission(resource.getPermission()) + .setCreateUserId(curUser.getId()); + permissions.add(permission); + } + saveBatch(permissions); + } + + /** + * 修改权限 + * + * @param permissionUpdateDTO 修改权限DTO + */ + @Override + public void update(PermissionUpdateDTO permissionUpdateDTO) { + UserContext curUser = userContextService.getCurUser(); + Permission permission = new Permission(); + BeanUtils.copyProperties(permissionUpdateDTO, permission); + for (Permission per : permissionUpdateDTO.getPermissions()) { + permission.setName(per.getName()); + permission.setPermission(per.getPermission()); + permission.setUpdateUserId(curUser.getId()); + permissionMapper.updateById(permission); + } + } + + /** + * 删除权限 + * + * @param permissionDeleteDTO 删除权限DTO + */ + @Override + public void delete(PermissionDeleteDTO permissionDeleteDTO) { + Set ids = new HashSet<>(); + List permissions = permissionMapper.selectList(new LambdaQueryWrapper().in(Permission::getId, permissionDeleteDTO.getIds())); + if (CollUtil.isNotEmpty(permissions)) { + for (Permission permission : permissions) { + if (permission.getPid() == 0) { + List permissionList = permissionMapper.findByPid(permission.getId()); + permissionList.forEach(x -> { + ids.add(x.getId()); + }); + } + ids.add(permission.getId()); + } + } + //解绑权限组权限 + authCodeMapper.untiedByPermissionId(ids); + permissionMapper.deleteBatchIds(ids); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RecycleTaskServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RecycleTaskServiceImpl.java new file mode 100644 index 0000000..c6d3acd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RecycleTaskServiceImpl.java @@ -0,0 +1,475 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpStatus; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.service.RecycleTaskService; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.api.impl.ShellFileStoreApiImpl; +import org.dubhe.biz.file.utils.IOUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.permission.base.BaseService; +import org.dubhe.recycle.config.RecycleConfig; +import org.dubhe.recycle.dao.RecycleDetailMapper; +import org.dubhe.recycle.dao.RecycleMapper; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.dto.RecycleTaskQueryDTO; +import org.dubhe.recycle.domain.entity.Recycle; +import org.dubhe.recycle.domain.entity.RecycleDetail; +import org.dubhe.recycle.enums.RecycleStatusEnum; +import org.dubhe.recycle.enums.RecycleTypeEnum; +import org.dubhe.recycle.service.RecycleService; +import org.dubhe.recycle.utils.RecycleTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.Resource; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +/** + * @description 垃圾回收 + * @date 2021-01-20 + */ +@Service +@RefreshScope +public class RecycleTaskServiceImpl implements RecycleTaskService { + + @Autowired + private RecycleMapper recycleMapper; + + @Autowired + private RecycleDetailMapper recycleDetailMapper; + + @Autowired + private UserContextService userContextService; + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + @Value("${storage.file-store}") + private String ip; + + @Value("${data.server.userName}") + private String userName; + + /** + * 资源回收单次执行任务数量限制(默认10000) + */ + @Value("${recycle.task.execute-limits:10000}") + private String taskExecuteLimits; + /** + * 资源无效文件临时存放目录(默认/tmp/tmp_) + */ + @Value("${recycle.file-tmp-path.invalid:/tmp/tmp_}") + private String invalidFileTmpPath; + /** + * 资源无效文件临时存放目录(默认/tmp/empty_) + */ + @Value("${recycle.file-tmp-path.recycle:/tmp/empty_}") + private String recycleFileTmpPath; + + @Autowired + private RecycleConfig recycleConfig; + + @Autowired + private RecycleService recycleService; + + @Autowired + private RecycleTool recycleTool; + + @Autowired + private RestTemplate restTemplate; + + /** + * 查询回收任务列表 + * + * @param recycleTaskQueryDTO 查询任务列表条件 + * @return Map 可回收任务列表 + */ + @Override + public Map getRecycleTasks(RecycleTaskQueryDTO recycleTaskQueryDTO) { + //获取当前用户信息 + Long curUserId = userContextService.getCurUserId(); + Page page = recycleTaskQueryDTO.toPage(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); + queryWrapper.in(CollectionUtil.isNotEmpty(recycleTaskQueryDTO.getRecycleTaskIdList()), Recycle::getId, recycleTaskQueryDTO.getRecycleTaskIdList()); + queryWrapper.eq(recycleTaskQueryDTO.getRecycleStatus() != null, Recycle::getRecycleStatus, recycleTaskQueryDTO.getRecycleStatus()); + queryWrapper.eq(recycleTaskQueryDTO.getRecycleModel() != null, Recycle::getRecycleModule, recycleTaskQueryDTO.getRecycleModel()); + if (!BaseService.isAdmin()) { + queryWrapper.eq(Recycle::getCreateUserId, curUserId); + } + queryWrapper.last(" ORDER BY recycle_delay_date DESC,update_time DESC "); + List recycleList = recycleMapper.selectPage(page, queryWrapper).getRecords(); + return PageUtil.toPage(page, recycleList); + } + + /** + * 获取垃圾回收任务列表 + * 资源回收单次执行任务数量限制(默认10000) + * @return List 垃圾回收任务列表 + */ + @Override + public List getRecycleTaskList() { + List recycleTaskList = recycleMapper.selectList(new LambdaQueryWrapper() + .in(Recycle::getRecycleStatus, Arrays.asList(RecycleStatusEnum.PENDING.getCode(), RecycleStatusEnum.FAILED.getCode())) + .le(Recycle::getRecycleDelayDate, DateUtil.format(new Date(), "yyyy-MM-dd")) + .last(" limit ".concat(taskExecuteLimits)) + ); + return recycleTaskList; + } + + /** + * 执行回收任务(单个) + * @param recycle 回收实体类 + * @param userId 当前操作用户 + */ + @Override + public void recycleTask(Recycle recycle, long userId) { + if (StrUtil.isNotEmpty(recycle.getRecycleCustom())) { + // 自定义回收 + customRecycle(recycle, userId, RecycleTool.BIZ_RECYCLE); + } else { + // 默认回收0 + defaultRecycle(recycle, userId); + } + } + + /** + * 获取任务详情 + * @param recycleId 回收任务ID + * @return List + */ + private List getDetail(long recycleId) { + return recycleDetailMapper.selectList(new LambdaQueryWrapper() + .eq(RecycleDetail::getRecycleId, recycleId) + ); + } + + /** + * 自定义回收远程调用 + * + * @param recycle 回收任务 + * @param userId 用户ID + * @param biz 模块业务 + */ + private void customRecycle(Recycle recycle, long userId, String biz) { + recycle.setUpdateUserId(userId); + RecycleCreateDTO recycleCreateDTO = RecycleCreateDTO.recycleTaskCreateDTO(recycle); + recycleCreateDTO.setDetailList(getDetail(recycle.getId())); + // 远程调用 + String url = RecycleTool.getCallUrl(recycleCreateDTO.getRecycleModule(), biz); + String token = recycleTool.generateToken(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add(RecycleTool.RECYCLE_TOKEN, token); + HttpEntity entity = new HttpEntity<>(JSON.toJSONString(recycleCreateDTO), headers); + ResponseEntity responseEntity = null; + try { + responseEntity = restTemplate.postForEntity(url, entity, DataResponseBody.class); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_SYS, "RecycleTaskServiceImpl customRecycle error :{}", e); + throw new BusinessException(responseEntity.getStatusCodeValue(), "自定义回收【" + biz + "】远程调用失败!"); + } + if (HttpStatus.HTTP_OK != responseEntity.getStatusCodeValue()) { + throw new BusinessException(responseEntity.getStatusCodeValue(), "自定义回收【" + biz + "】远程调用失败!"); + } + DataResponseBody dataResponseBody = responseEntity.getBody(); + if (!dataResponseBody.succeed()) { + throw new BusinessException(dataResponseBody.getCode(), dataResponseBody.getMsg()); + } + } + + /** + * 默认回收 + * (无统一事务操作) + * + * @param recycle 回收任务 + * @param userId 用户ID + */ + private void defaultRecycle(Recycle recycle, long userId) { + try { + List detailList = getDetail(recycle.getId()); + recycleService.updateRecycle(recycle, RecycleStatusEnum.DOING, null, userId); + for (RecycleDetail detail : detailList) { + if (Objects.equals(RecycleTypeEnum.FILE.getCode(), detail.getRecycleType())) { + // 文件回收 + if (!defaultRecycleFile(detail, userId)) { + recycleService.updateRecycle(recycle, RecycleStatusEnum.FAILED, "文件回收失败!" + detail.getId(), userId); + return; + } + } else { + // 其他资源(数据库资源)回收 + if (!defaultRecycleOthers(detail, userId)) { + recycleService.updateRecycle(recycle, RecycleStatusEnum.FAILED, "不支持非文件资源默认回收方式!" + detail.getId(), userId); + return; + } + } + } + recycleService.updateRecycle(recycle, RecycleStatusEnum.SUCCEEDED, null, userId); + } catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "默认回收失败!{}", e); + recycleService.updateRecycle(recycle, RecycleStatusEnum.FAILED, e.getMessage(), userId); + } + } + + + /** + * 默认方式文件回收 + * @param detail 回收任务详情 + * @param userId 用户ID + * @return true 回收成功, false 回收失败 + */ + private boolean defaultRecycleFile(RecycleDetail detail, long userId) { + if (RecycleStatusEnum.SUCCEEDED.getCode().equals(detail.getRecycleStatus())) { + // 已经删除成功,无需再执行 + return true; + } + String errMsg = deleteFileByCMD(detail.getRecycleCondition(), detail.getId().toString()); + recycleService.updateRecycleDetail(detail, + StrUtil.isEmpty(errMsg) ? RecycleStatusEnum.SUCCEEDED : RecycleStatusEnum.FAILED, + errMsg, + userId + ); + return StrUtil.isEmpty(errMsg); + } + + /** + * 默认方式回收其他资源(暂不支持) + * @param detail 回收任务详情 + * @param userId 用户ID + * @return false 回收失败 + */ + private boolean defaultRecycleOthers(RecycleDetail detail, long userId) { + LogUtil.warn(LogEnum.GARBAGE_RECYCLE, "recycle task id:{} is not support", detail.getId()); + recycleService.updateRecycleDetail(detail, RecycleStatusEnum.FAILED, "不支持非文件资源默认回收方式!", userId); + return false; + } + + + /** + * 实时删除临时目录完整路径无效文件 + * + * @param sourcePath 删除路径 + */ + @Override + public void delTempInvalidResources(String sourcePath) { + if (!BaseService.isAdmin(userContextService.getCurUser())) { + throw new BusinessException(ResponseCode.UNAUTHORIZED, "不支持普通用户操作"); + } + String resMsg = deleteFileByCMD(sourcePath, RandomUtil.randomString(MagicNumConstant.TWO)); + if (StrUtil.isNotEmpty(resMsg)) { + throw new BusinessException(ResponseCode.ERROR, resMsg); + } + } + + /** + * 回收天枢一站式平台中的无效文件资源 + * 处理方式:获取到回收任务表中的无效文件路径,通过linux命令进行具体删除 + * 文件路径必须满足格式如:/nfs/当前系统环境/具体删除的文件或文件夹(至少三层目录) + * @param recycleConditionPath 文件回收绝对路径 + * @param randomPath emptyDir目录补偿位置 + * @return String 回收任务失败返回的失败信息 + */ + private String deleteFileByCMD(String recycleConditionPath, String randomPath) { + String sourcePath = fileStoreApi.formatPath(recycleConditionPath); + //判断该路径是否存在文件或文件夹 + String nfsBucket = fileStoreApi.formatPath(fileStoreApi.getRootDir() + fileStoreApi.getBucket() + File.separator); + sourcePath = sourcePath.endsWith(File.separator) ? sourcePath : sourcePath + File.separator; + try { + //校验回收文件是否存在以及回收文件必须至少在当前环境目录下还有一层目录,如:/nfs/dubhe-test/xxxx/ + if (sourcePath.startsWith(nfsBucket) + && sourcePath.length() > nfsBucket.length()) { + if (!fileStoreApi.fileOrDirIsExist(sourcePath)) { + // 文件不存在,即认为已删除成功 + return null; + } + String emptyDir = recycleFileTmpPath + randomPath + File.separator; + LogUtil.debug(LogEnum.GARBAGE_RECYCLE, "recycle task sourcePath:{},emptyDir:{}", sourcePath, emptyDir); + Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", String.format(ShellFileStoreApiImpl.DEL_COMMAND, userName, ip, emptyDir, emptyDir, sourcePath, emptyDir, sourcePath)}); + return processRecycle(process); + } else { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "file recycle is failed! sourcePath:{}", sourcePath); + return "文件资源回收失败! sourcePath:" + sourcePath; + } + } catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "file recycle is failed! Exception:{}", e); + return "文件资源回收失败! sourcePath:" + sourcePath + " Exception:" + e.getMessage(); + } + } + + /** + * 执行服务器命令 + * + * @param process Process对象 + * @return null 成功执行,其他:异常结束信息 + */ + private String processRecycle(Process process) { + InputStreamReader stream = new InputStreamReader(process.getErrorStream()); + BufferedReader reader = new BufferedReader(stream); + StringBuilder errMessage = new StringBuilder(); + try { + while (reader.read() != MagicNumConstant.NEGATIVE_ONE) { + errMessage.append(reader.readLine()); + } + int status = process.waitFor(); + if (status == 0) { + // 成功 + return null; + } else { + // 失败 + LogUtil.info(LogEnum.GARBAGE_RECYCLE, "recycleSourceIsOk is failure,errorMsg:{},processStatus:{}", errMessage.toString(), status); + return errMessage.length() > 0 ? errMessage.toString() : "文件删除失败!"; + } + } catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "recycleSourceIsOk is failure: {} ", e); + return e.getMessage(); + } finally { + IOUtil.close(reader, stream); + } + } + + /** + * 立即执行回收任务 + * + * @param taskId 回收任务ID + */ + @Override + public void recycleTaskResources(long taskId) { + //根据taskId查询回收任务 + Recycle recycle = getRecycleTask(taskId, Arrays.asList(RecycleStatusEnum.PENDING.getCode(), RecycleStatusEnum.FAILED.getCode())); + //执行回收任务 + recycleTask(recycle, recycle.getUpdateUserId()); + } + + /** + * 获取任务并校验 + * 更新修改人ID为当前操作人 + * + * @param taskId 回收任务ID + * @param statusEnumList 回收状态 + * @return Recycle + */ + private Recycle getRecycleTask(long taskId, List statusEnumList) { + //根据taskId查询回收任务 + Recycle recycle = recycleMapper.selectOne(new LambdaQueryWrapper() + .eq(Recycle::getId, taskId) + .in(CollectionUtil.isNotEmpty(statusEnumList), Recycle::getRecycleStatus, statusEnumList)); + if (recycle == null) { + throw new BusinessException("未查询到回收任务"); + } + UserContext curUser = userContextService.getCurUser(); + //只有创建该任务用户或管理员有权限实时执行回收任务 + if (!recycle.getCreateUserId().equals(curUser.getId()) && !BaseService.isAdmin(curUser)) { + throw new BusinessException("没有权限操作"); + } + recycle.setUpdateUserId(curUser.getId()); + return recycle; + } + + + /** + * 还原回收任务 + * + * @param taskId 回收任务ID + */ + @Override + public void restore(long taskId) { + //根据taskId查询回收任务 + Recycle recycle = getRecycleTask(taskId, Arrays.asList(RecycleStatusEnum.PENDING.getCode(), RecycleStatusEnum.FAILED.getCode())); + if (StrUtil.isEmpty(recycle.getRestoreCustom())) { + throw new BusinessException("仅支持自定义还原!"); + } else { + long userId = recycle.getUpdateUserId(); + // 自定义还原 + customRecycle(recycle, userId, RecycleTool.BIZ_RESTORE); + } + } + + /** + * 根据路径回收无效文件 + * + * @param sourcePath 文件路径 + */ + @Override + public void deleteInvalidResourcesByCMD(String sourcePath) { + //判断该路径是否存在文件或文件夹 + if (!fileStoreApi.fileOrDirIsExist(sourcePath)) { + return; + } + File file = new File(sourcePath); + File[] files = file.listFiles(); + if (files != null && files.length != 0) { + for (File f : files) { + //获取文件夹命名(userId) + String fileName = f.getName(); + if (!f.isDirectory()) { + continue; + } + File[] director = f.listFiles(); + if (director != null && director.length != 0) { + for (File directory : director) { + //获取文件夹命名(时间戳+4位随机数) + String directoryName = directory.getName(); + //如果文件上传时长大于最大有效时间,则删除 + if ((System.currentTimeMillis() - directory.lastModified()) >= recycleConfig.getFileValid() * MagicNumConstant.SIXTY * MagicNumConstant.SIXTY * MagicNumConstant.ONE_THOUSAND) { + try { + String delRealPath = fileStoreApi.formatPath(sourcePath + File.separator + fileName + File.separator + directoryName); + delRealPath = delRealPath.endsWith(File.separator) ? delRealPath : delRealPath + File.separator; + String emptyDir = invalidFileTmpPath + directoryName + File.separator; + Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", String.format(ShellFileStoreApiImpl.DEL_COMMAND, userName, ip, emptyDir, emptyDir, delRealPath, emptyDir, delRealPath)}); + Integer deleteStatus = process.waitFor(); + LogUtil.info(LogEnum.GARBAGE_RECYCLE, "recycle resources path:{},recycle status:{}", delRealPath, deleteStatus); + } catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "recycle invalid resources error:{}", e); + } + } + } + } + } + } + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/ResourceSpecsServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/ResourceSpecsServiceImpl.java new file mode 100644 index 0000000..b3ad0da --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/ResourceSpecsServiceImpl.java @@ -0,0 +1,209 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.collections4.CollectionUtils; +import org.dubhe.admin.dao.ResourceSpecsMapper; +import org.dubhe.admin.domain.dto.ResourceSpecsCreateDTO; +import org.dubhe.admin.domain.dto.ResourceSpecsDeleteDTO; +import org.dubhe.admin.domain.dto.ResourceSpecsQueryDTO; +import org.dubhe.admin.domain.dto.ResourceSpecsUpdateDTO; +import org.dubhe.admin.domain.entity.ResourceSpecs; +import org.dubhe.admin.domain.vo.ResourceSpecsQueryVO; +import org.dubhe.admin.service.ResourceSpecsService; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.QueryResourceSpecsDTO; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.base.vo.QueryResourceSpecsVO; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description CPU, GPU, 内存等资源规格管理 + * @date 2021-05-27 + */ +@Service +public class ResourceSpecsServiceImpl implements ResourceSpecsService { + + @Autowired + private ResourceSpecsMapper resourceSpecsMapper; + + @Autowired + private UserContextService userContextService; + + /** + * 查询资源规格 + * @param resourceSpecsQueryDTO 查询资源规格请求实体 + * @return List resourceSpecs 资源规格列表 + */ + @Override + public Map getResourceSpecs(ResourceSpecsQueryDTO resourceSpecsQueryDTO) { + Page page = resourceSpecsQueryDTO.toPage(); + //排序字段 + String sort = null == resourceSpecsQueryDTO.getSort() ? StringConstant.CREATE_TIME_SQL : resourceSpecsQueryDTO.getSort(); + QueryWrapper queryResourceSpecsWrapper = new QueryWrapper<>(); + queryResourceSpecsWrapper.like(resourceSpecsQueryDTO.getSpecsName() != null, "specs_name", resourceSpecsQueryDTO.getSpecsName()) + .eq(resourceSpecsQueryDTO.getResourcesPoolType() != null, "resources_pool_type", resourceSpecsQueryDTO.getResourcesPoolType()) + .eq(resourceSpecsQueryDTO.getModule() != null, "module", resourceSpecsQueryDTO.getModule()); + if (StringConstant.SORT_ASC.equals(resourceSpecsQueryDTO.getOrder())) { + queryResourceSpecsWrapper.orderByAsc(StringUtils.humpToLine(sort)); + } else { + queryResourceSpecsWrapper.orderByDesc(StringUtils.humpToLine(sort)); + } + Page pageResourceSpecsResult = resourceSpecsMapper.selectPage(page, queryResourceSpecsWrapper); + //结果集处理 + //查询结果数 + page.setTotal(pageResourceSpecsResult.getTotal()); + List resourceSpecs = pageResourceSpecsResult.getRecords(); + List resourceSpecsQueryVOS = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(resourceSpecs)) { + resourceSpecsQueryVOS = resourceSpecs.stream().map(x -> { + ResourceSpecsQueryVO resourceSpecsQueryVO = new ResourceSpecsQueryVO(); + BeanUtils.copyProperties(x, resourceSpecsQueryVO); + return resourceSpecsQueryVO; + }).collect(Collectors.toList()); + } + return PageUtil.toPage(page, resourceSpecsQueryVOS); + } + + /** + * 新增资源规格 + * @param resourceSpecsCreateDTO 新增资源规格实体 + * @return List 新增资源规格id + */ + @Override + @Transactional(rollbackFor = Exception.class) + public List create(ResourceSpecsCreateDTO resourceSpecsCreateDTO) { + UserContext curUser = userContextService.getCurUser(); + //规格名称校验 + QueryWrapper specsWrapper = new QueryWrapper<>(); + specsWrapper.eq("specs_name", resourceSpecsCreateDTO.getSpecsName()).eq("module", resourceSpecsCreateDTO.getModule()); + if (resourceSpecsMapper.selectCount(specsWrapper) > 0) { + LogUtil.error(LogEnum.SYS_ERR, "The module: {} resourceSpecs name ({}) already exists", resourceSpecsCreateDTO.getModule(), resourceSpecsCreateDTO.getSpecsName()); + throw new BusinessException("规格名称已存在"); + } + ResourceSpecs resourceSpecs = new ResourceSpecs(); + BeanUtils.copyProperties(resourceSpecsCreateDTO, resourceSpecs); + resourceSpecs.setCreateUserId(curUser.getId()); + if (resourceSpecsCreateDTO.getGpuNum() > 0) { + resourceSpecs.setResourcesPoolType(true); + } + try { + resourceSpecsMapper.insert(resourceSpecs); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "The user {} saved the ResourceSpecs parameters ResourceSpecsCreateDTO: {} was not successful. Failure reason :{}", curUser, resourceSpecsCreateDTO, e); + throw new BusinessException("内部错误"); + } + return Collections.singletonList(resourceSpecs.getId()); + } + + /** + * 修改资源规格 + * @param resourceSpecsUpdateDTO 修改资源规格实体 + * @return List 修改资源规格id + */ + @Override + @Transactional(rollbackFor = Exception.class) + public List update(ResourceSpecsUpdateDTO resourceSpecsUpdateDTO) { + UserContext curUser = userContextService.getCurUser(); + ResourceSpecs resourceSpecs = new ResourceSpecs(); + resourceSpecs.setId(resourceSpecsUpdateDTO.getId()).setUpdateUserId(curUser.getId()); + if (resourceSpecsUpdateDTO.getSpecsName() != null) { + //规格名称校验 + QueryWrapper specsWrapper = new QueryWrapper<>(); + specsWrapper.eq("specs_name", resourceSpecsUpdateDTO.getSpecsName()).eq("module", resourceSpecsUpdateDTO.getModule()).ne("id", resourceSpecsUpdateDTO.getId()); + if (resourceSpecsMapper.selectCount(specsWrapper) > 0) { + LogUtil.error(LogEnum.SYS_ERR, "The module: {} resourceSpecs name ({}) already exists", resourceSpecsUpdateDTO.getModule(), resourceSpecsUpdateDTO.getSpecsName()); + throw new BusinessException("规格名称已存在"); + } + resourceSpecs.setSpecsName(resourceSpecsUpdateDTO.getSpecsName()); + } + if (resourceSpecsUpdateDTO.getCpuNum() != null) { + resourceSpecs.setCpuNum(resourceSpecsUpdateDTO.getCpuNum()); + } + if (resourceSpecsUpdateDTO.getGpuNum() != null) { + resourceSpecs.setGpuNum(resourceSpecsUpdateDTO.getGpuNum()); + if (resourceSpecsUpdateDTO.getGpuNum() > 0) { + resourceSpecs.setResourcesPoolType(true); + } else { + resourceSpecs.setResourcesPoolType(false); + } + } + if (resourceSpecsUpdateDTO.getMemNum() != null) { + resourceSpecs.setMemNum(resourceSpecsUpdateDTO.getMemNum()); + } + if (resourceSpecsUpdateDTO.getWorkspaceRequest() != null) { + resourceSpecs.setWorkspaceRequest(resourceSpecsUpdateDTO.getWorkspaceRequest()); + } + try { + resourceSpecsMapper.updateById(resourceSpecs); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "The user {} updated the ResourceSpecs parameters resourceSpecsUpdateDTO: {} was not successful. Failure reason :{}", curUser, resourceSpecsUpdateDTO, e); + throw new BusinessException("内部错误"); + } + return Collections.singletonList(resourceSpecs.getId()); + } + + /** + * 资源规格删除 + * @param resourceSpecsDeleteDTO 资源规格删除id集合 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(ResourceSpecsDeleteDTO resourceSpecsDeleteDTO) { + UserContext curUser = userContextService.getCurUser(); + Set idList = resourceSpecsDeleteDTO.getIds(); + try { + resourceSpecsMapper.deleteBatchIds(idList); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "The user {} Deleted the ResourceSpecs parameters resourceSpecsDeleteDTO: {} was not successful. Failure reason :{}", curUser, resourceSpecsDeleteDTO, e); + throw new BusinessException("内部错误"); + } + } + + /** + * 查询资源规格 + * @param queryResourceSpecsDTO 查询资源规格请求实体 + * @return QueryResourceSpecsVO 资源规格返回结果实体类 + */ + @Override + public QueryResourceSpecsVO queryResourceSpecs(QueryResourceSpecsDTO queryResourceSpecsDTO) { + QueryWrapper queryResourceSpecsWrapper = new QueryWrapper<>(); + queryResourceSpecsWrapper.eq("specs_name", queryResourceSpecsDTO.getSpecsName()) + .eq("module", queryResourceSpecsDTO.getModule()); + ResourceSpecs resourceSpecs = resourceSpecsMapper.selectOne(queryResourceSpecsWrapper); + if (resourceSpecs == null) { + throw new BusinessException("资源规格不存在或已被删除"); + } + QueryResourceSpecsVO queryResourceSpecsVO = new QueryResourceSpecsVO(); + BeanUtils.copyProperties(resourceSpecs, queryResourceSpecsVO); + return queryResourceSpecsVO; + } +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RoleServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..df3f277 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/RoleServiceImpl.java @@ -0,0 +1,276 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.admin.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dubhe.admin.dao.RoleMapper; +import org.dubhe.admin.domain.dto.RoleCreateDTO; +import org.dubhe.admin.domain.dto.RoleDTO; +import org.dubhe.admin.domain.dto.RoleQueryDTO; +import org.dubhe.admin.domain.dto.RoleSmallDTO; +import org.dubhe.admin.domain.dto.RoleUpdateDTO; +import org.dubhe.admin.domain.entity.Menu; +import org.dubhe.admin.domain.entity.Role; +import org.dubhe.admin.service.RoleService; +import org.dubhe.admin.service.convert.RoleConvert; +import org.dubhe.admin.service.convert.RoleSmallConvert; +import org.dubhe.biz.base.constant.UserConstant; +import org.dubhe.biz.base.enums.BaseErrorCodeEnum; +import org.dubhe.biz.base.enums.SwitchEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.file.utils.DubheFileUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * @description 角色服务 实现类 + * @date 2020-06-01 + */ +@Service +public class RoleServiceImpl extends ServiceImpl implements RoleService { + + @Autowired + private RoleMapper roleMapper; + + @Autowired + private RoleConvert roleConvert; + + @Autowired + private RoleSmallConvert roleSmallConvert; + + /** + * 按条件查询角色信息 + * + * @param criteria 角色查询条件 + * @return java.util.List 角色信息返回实例 + */ + @Override + public List queryAllSmall(RoleQueryDTO criteria) { + return roleSmallConvert.toDto(roleMapper.selectList((WrapperHelp.getWrapper(criteria)))); + } + + + /** + * 按条件查询角色列表 + * + * @param criteria 角色查询条件 + * @return java.util.List 角色信息返回实例 + */ + @Override + public List queryAll(RoleQueryDTO criteria) { + return roleConvert.toDto(roleMapper.selectCollList(WrapperHelp.getWrapper(criteria))); + } + + /** + * 分页查询角色列表 + * + * @param criteria 角色查询条件 + * @param page 分页实体 + * @return java.lang.Object 角色信息返回实例 + */ + @Override + public Object queryAll(RoleQueryDTO criteria, Page page) { + IPage roles = roleMapper.selectCollPage(page, WrapperHelp.getWrapper(criteria)); + return PageUtil.toPage(roles, roleConvert::toDto); + } + + /** + * 根据ID查询角色信息 + * + * @param id id + * @return org.dubhe.domain.dto.RoleDTO 角色信息 + */ + @Override + public RoleDTO findById(long id) { + Role role = roleMapper.selectCollById(id); + return roleConvert.toDto(role); + } + + /** + * 新增角色 + * + * @param resources 角色新增请求实体 + * @return org.dubhe.domain.dto.RoleDTO 角色返回实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public RoleDTO create(RoleCreateDTO resources) { + if (!Objects.isNull(roleMapper.findByName(resources.getName()))) { + throw new BusinessException("角色名已存在"); + } + Role role = Role.builder().build(); + BeanUtils.copyProperties(resources, role); + roleMapper.insert(role); + //新创建角色分配默认的【概览】菜单 + roleMapper.tiedRoleMenu(role.getId(), 1L); + return roleConvert.toDto(role); + } + + + /** + * 修改角色 + * + * @param resources 角色修改请求实体 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(RoleUpdateDTO resources) { + + Role roleTmp = roleMapper.findByName(resources.getName()); + if (!Objects.isNull(roleTmp) && !roleTmp.getId().equals(resources.getId())) { + throw new BusinessException("角色名已存在"); + } + + Role role = Role.builder().build(); + BeanUtils.copyProperties(resources, role); + roleMapper.updateById(role); + } + + + /** + * 修改角色菜单 + * + * @param resources 角色菜单请求实体 + * @param roleDTO 角色请求实体 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMenu(RoleUpdateDTO resources, RoleDTO roleDTO) { + Role role = roleConvert.toEntity(roleDTO); + role.setMenus(resources.getMenus()); + roleMapper.untiedRoleMenuByRoleId(role.getId()); + for (Menu menu : resources.getMenus()) { + roleMapper.tiedRoleMenu(role.getId(), menu.getId()); + } + } + + /** + * 删除角色菜单 + * + * @param id 角色id + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void untiedMenu(Long id) { + roleMapper.untiedRoleMenuByMenuId(id); + } + + /** + * 批量删除角色 + * + * @param ids 角色ids + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + + if (!CollectionUtils.isEmpty(ids)) { + // admin 角色和 register 角色不可删除 + if (ids.contains(Long.valueOf(UserConstant.ADMIN_ROLE_ID)) || + ids.contains(Long.valueOf(UserConstant.REGISTER_ROLE_ID))) { + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_ROLE_CANNOT_DELETE); + } + + for (Long id : ids) { + roleMapper.untiedUserRoleByRoleId(id); + roleMapper.untiedRoleMenuByRoleId(id); + roleMapper.updateById( + Role.builder() + .id(id) + .deleted(SwitchEnum.getBooleanValue(SwitchEnum.ON.getValue())).build() + ); + } + + } + + + } + + /** + * 导出角色信息 + * + * @param roles 角色列表 + * @param response + */ + @Override + public void download(List roles, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (RoleDTO role : roles) { + Map map = new LinkedHashMap<>(); + map.put("角色名称", role.getName()); + map.put("默认权限", role.getPermission()); + map.put("描述", role.getRemark()); + map.put("创建日期", role.getCreateTime()); + list.add(map); + } + DubheFileUtil.downloadExcel(list, response); + } + + + /** + * 根据用户ID获取角色信息 + * + * @param userId 用户ID + * @return java.util.List 角色列表 + */ + @Override + public List getRoleByUserId(Long userId) { + List list = roleMapper.findRolesByUserId(userId); + return roleSmallConvert.toDto(list); + } + + + /** + * 获取角色列表 + * + * @param userId 用户ID + * @param teamId 团队ID + * @return java.util.List 角色列表 + */ + @Override + public List getRoleByUserIdAndTeamId(Long userId, Long teamId) { + List list = roleMapper.findByUserIdAndTeamId(userId, teamId); + return roleSmallConvert.toDto(list); + } + + /** + * 新增角色菜单 + * + * @param roleId 角色ID + * @param menuId 菜单ID + */ + @Override + public void tiedRoleMenu(Long roleId, Long menuId) { + roleMapper.tiedRoleMenu(roleId, menuId); + } + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/TeamServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/TeamServiceImpl.java new file mode 100644 index 0000000..9b6f85d --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/TeamServiceImpl.java @@ -0,0 +1,169 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.dao.TeamMapper; +import org.dubhe.admin.domain.dto.TeamCreateDTO; +import org.dubhe.biz.base.dto.TeamDTO; +import org.dubhe.admin.domain.dto.TeamQueryDTO; +import org.dubhe.admin.domain.dto.TeamUpdateDTO; +import org.dubhe.admin.domain.entity.Team; +import org.dubhe.admin.service.TeamService; +import org.dubhe.admin.service.convert.TeamConvert; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.file.utils.DubheFileUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * @description 团队服务 实现类 + * @date 2020-06-01 + */ +@Service +public class TeamServiceImpl implements TeamService { + + @Autowired + private TeamMapper teamMapper; + + @Autowired + private TeamConvert teamConvert; + + + /** + * 获取团队列表 + * + * @param criteria 查询团队列表条件 + * @return java.util.List 团队列表条件 + */ + @Override + public List queryAll(TeamQueryDTO criteria) { + return teamConvert.toDto(teamMapper.selectList(WrapperHelp.getWrapper(criteria))); + } + + + /** + * 分页查询团队列表 + * + * @param criteria 查询请求条件 + * @param page 分页实体 + * @return java.lang.Object 团队列表 + */ + @Override + public Object queryAll(TeamQueryDTO criteria, Page page) { + IPage teamList = teamMapper.selectPage(page, WrapperHelp.getWrapper(criteria)); + return PageUtil.toPage(teamList, teamConvert::toDto); + } + + + /** + * 查询团队列表 + * + * @param page 分页请求实体 + * @return java.util.List 团队列表 + */ + @Override + public List queryAll(Page page) { + IPage teamList = teamMapper.selectPage(page, null); + return teamConvert.toDto(teamList.getRecords()); + } + + /** + * 根据ID插叙团队信息 + * + * @param id id + * @return org.dubhe.domain.dto.TeamDTO 团队返回实例 + */ + @Override + public TeamDTO findById(Long id) { + Team team = teamMapper.selectCollById(id); + return teamConvert.toDto(team); + } + + + /** + * 新增团队信息 + * + * @param resources 团队新增请求实体 + * @return org.dubhe.domain.dto.TeamDTO 团队返回实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public TeamDTO create(TeamCreateDTO resources) { + Team team = Team.builder().build(); + BeanUtils.copyProperties(resources, team); + teamMapper.insert(team); + return teamConvert.toDto(team); + } + + + /** + * 修改团队 + * + * @param resources 团队修改请求实体 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(TeamUpdateDTO resources) { + Team team = Team.builder().build(); + BeanUtils.copyProperties(resources, team); + teamMapper.updateById(team); + } + + /** + * 团队删除 + * + * @param teamDtos 团队删除列表 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set teamDtos) { + for (TeamDTO teamDto : teamDtos) { + teamMapper.deleteById(teamDto.getId()); + } + } + + /** + * 团队信息导出 + * + * @param teamDtos 团队列表 + * @param response 导出http响应 + */ + @Override + public void download(List teamDtos, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (TeamDTO teamDTO : teamDtos) { + Map map = new LinkedHashMap<>(); + map.put("名称", teamDTO.getName()); + map.put("状态", teamDTO.getEnabled() ? "启用" : "停用"); + map.put("创建日期", teamDTO.getCreateTime()); + list.add(map); + } + DubheFileUtil.downloadExcel(list, response); + } + + +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserGroupServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserGroupServiceImpl.java new file mode 100644 index 0000000..8c68435 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserGroupServiceImpl.java @@ -0,0 +1,292 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.dao.UserGroupMapper; +import org.dubhe.admin.dao.UserRoleMapper; +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.entity.Group; +import org.dubhe.admin.domain.entity.User; +import org.dubhe.admin.domain.entity.UserRole; +import org.dubhe.admin.domain.vo.UserGroupVO; +import org.dubhe.admin.service.UserGroupService; +import org.dubhe.admin.service.UserService; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.ReflectionUtils; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description 用户组服务实现类 + * @date 2021-05-06 + */ +@Service +public class UserGroupServiceImpl implements UserGroupService { + + @Autowired + private UserGroupMapper userGroupMapper; + + @Autowired + private UserContextService userContextService; + + @Autowired + private UserService userService; + + @Autowired + private UserRoleMapper userRoleMapper; + + public final static List FIELD_NAMES; + + static { + FIELD_NAMES = ReflectionUtils.getFieldNames(UserGroupVO.class); + } + + + /** + * 分页查询用户组列表 + * + * @param queryDTO 查询实体DTO + * @return Map 用户组及分页信息 + */ + @Override + public Map queryAll(UserGroupQueryDTO queryDTO) { + + Page page = queryDTO.toPage(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (StringUtils.isNotEmpty(queryDTO.getKeyword())) { + queryWrapper.and(x -> x.eq("id", queryDTO.getKeyword()).or().like("name", queryDTO.getKeyword())); + } + + //排序 + IPage groupList; + try { + if (queryDTO.getSort() != null && FIELD_NAMES.contains(queryDTO.getSort())) { + if (StringConstant.SORT_ASC.equalsIgnoreCase(queryDTO.getOrder())) { + queryWrapper.orderByAsc(StringUtils.humpToLine(queryDTO.getSort())); + } else { + queryWrapper.orderByDesc(StringUtils.humpToLine(queryDTO.getSort())); + } + } else { + queryWrapper.orderByDesc(StringConstant.ID); + } + groupList = userGroupMapper.selectPage(page, queryWrapper); + } catch (Exception e) { + LogUtil.error(LogEnum.IMAGE, "query image list display exception {}", e); + throw new BusinessException("查询用户组列表展示异常"); + } + List userGroupResult = groupList.getRecords().stream().map(x -> { + UserGroupVO userGroupVO = new UserGroupVO(); + BeanUtils.copyProperties(x, userGroupVO); + return userGroupVO; + }).collect(Collectors.toList()); + return PageUtil.toPage(page, userGroupResult); + } + + /** + * 新增用户组 + * + * @param groupCreateDTO 新增用户组实体DTO + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Group create(UserGroupDTO groupCreateDTO) { + //获取当前用户 + UserContext curUser = userContextService.getCurUser(); + Group userGroup = new Group(); + try { + BeanUtils.copyProperties(groupCreateDTO, userGroup); + userGroup.setCreateUserId(curUser.getId()); + userGroupMapper.insert(userGroup); + } catch (DuplicateKeyException e) { + throw new BusinessException("用户组名称不能重复"); + } + return userGroup; + } + + /** + * 修改用户组信息 + * + * @param groupUpdateDTO 修改用户组实体DTO + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(UserGroupDTO groupUpdateDTO) { + //获取当前用户 + UserContext curUser = userContextService.getCurUser(); + + Group userGroup = new Group(); + BeanUtils.copyProperties(groupUpdateDTO, userGroup); + userGroup.setUpdateUserId(curUser.getId()); + userGroupMapper.updateById(userGroup); + + } + + /** + * 删除用户组 + * + * @param ids 用户组ids + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (Long id : ids) { + userGroupMapper.delUserByGroupId(id); + userGroupMapper.delUserGroupByGroupId(id); + } + } + + /** + * 修改用户组成员 + * + * @param userGroupUpdDTO 新增组用户DTO实体 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updUserWithGroup(UserGroupUpdDTO userGroupUpdDTO) { + if (userGroupUpdDTO.getGroupId() != null) { + userGroupMapper.delUserByGroupId(userGroupUpdDTO.getGroupId()); + for (Long userId : userGroupUpdDTO.getUserIds()) { + userGroupMapper.addUserWithGroup(userGroupUpdDTO.getGroupId(), userId); + } + } + } + + /** + * 删除用户组成员 + * + * @param userGroupDelDTO 删除用户组成员 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delUserWithGroup(UserGroupUpdDTO userGroupDelDTO) { + for (Long userId : userGroupDelDTO.getUserIds()) { + userGroupMapper.delUserWithGroup(userGroupDelDTO.getGroupId(), userId); + } + } + + + /** + * 获取没有归属组的用户 + * + * @return List 没有归属组的用户 + */ + @Override + public List findUserWithOutGroup() { + return userGroupMapper.findUserWithOutGroup(); + } + + + /** + * 获取用户组成员信息 + * + * @param groupId 用户组id + * @return List 用户列表 + */ + @Override + public List queryUserByGroupId(Long groupId) { + return userGroupMapper.queryUserByGroupId(groupId); + } + + /** + * 批量修改用户组成员的状态 + * + * @param userStateUpdateDTO 实体DTO + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserState(UserStateUpdateDTO userStateUpdateDTO) { + //获取用户组的成员id + List userList = userGroupMapper.queryUserByGroupId(userStateUpdateDTO.getGroupId()); + Set ids = new HashSet<>(); + if (CollUtil.isNotEmpty(userList)) { + for (User user : userList) { + ids.add(user.getId()); + } + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.in(User::getId, ids); + updateWrapper.set(User::getEnabled, userStateUpdateDTO.isEnabled()); + userService.update(updateWrapper); + } + + /** + * 批量删除用户组用户 + * + * @param userGroupUpdDTO 批量删除用户组用户DTO + */ + @Override + public void delUser(UserGroupUpdDTO userGroupUpdDTO) { + //获取用户组的成员id + List userList = userGroupMapper.queryUserByGroupId(userGroupUpdDTO.getGroupId()); + userGroupMapper.delUserByGroupId(userGroupUpdDTO.getGroupId()); + Set ids = new HashSet<>(); + if (CollUtil.isNotEmpty(userList)) { + for (User user : userList) { + ids.add(user.getId()); + } + } + userService.delete(ids); + } + + /** + * 批量修改用户组用户的角色 + * + * @param userRoleUpdateDTO + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserRole(UserRoleUpdateDTO userRoleUpdateDTO) { + //获取用户组的成员id + List userList = userGroupMapper.queryUserByGroupId(userRoleUpdateDTO.getGroupId()); + List userRoleList = new ArrayList<>(); + Set ids = new HashSet<>(); + if (CollUtil.isNotEmpty(userList)) { + for (User user : userList) { + ids.add(user.getId()); + for (Long roleId : userRoleUpdateDTO.getRoleIds()) { + UserRole userRole = new UserRole(); + userRole.setUserId(user.getId()); + userRole.setRoleId(roleId); + userRoleList.add(userRole); + } + } + } + //清空当前用户 + userRoleMapper.deleteByUserId(ids); + //添加用户的新角色 + userRoleMapper.insertBatchs(userRoleList); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserServiceImpl.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..5fbe9d3 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/service/impl/UserServiceImpl.java @@ -0,0 +1,907 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dubhe.admin.client.AuthServiceClient; +import org.dubhe.admin.dao.*; +import org.dubhe.admin.domain.dto.*; +import org.dubhe.admin.domain.entity.Role; +import org.dubhe.admin.domain.entity.User; +import org.dubhe.admin.domain.entity.UserAvatar; +import org.dubhe.admin.domain.entity.UserRole; +import org.dubhe.admin.domain.vo.EmailVo; +import org.dubhe.admin.domain.vo.UserVO; +import org.dubhe.admin.enums.UserMailCodeEnum; +import org.dubhe.admin.event.EmailEventPublisher; +import org.dubhe.admin.service.UserService; +import org.dubhe.admin.service.convert.TeamConvert; +import org.dubhe.admin.service.convert.UserConvert; +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.constant.UserConstant; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.*; +import org.dubhe.biz.base.enums.BaseErrorCodeEnum; +import org.dubhe.biz.base.enums.SwitchEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.exception.CaptchaException; +import org.dubhe.biz.base.utils.DateUtil; +import org.dubhe.biz.base.utils.Md5Util; +import org.dubhe.biz.base.utils.RandomUtil; +import org.dubhe.biz.base.utils.RsaEncrypt; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.file.utils.DubheFileUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.permission.annotation.DataPermissionMethod; +import org.dubhe.biz.redis.utils.RedisUtils; +import org.dubhe.cloud.authconfig.dto.JwtUserDTO; +import org.dubhe.cloud.authconfig.factory.PasswordEncoderFactory; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cglib.beans.BeanMap; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description Demo服务接口实现类 + * @date 2020-11-26 + */ +@Service +public class UserServiceImpl extends ServiceImpl implements UserService { + + @Value("${rsa.private_key}") + private String privateKey; + + @Value("${initial_password}") + private String initialPassword; + + @Autowired + private UserMapper userMapper; + + + @Resource + private RoleMapper roleMapper; + + @Autowired + private MenuMapper menuMapper; + + @Autowired + private TeamMapper teamMapper; + + @Autowired + private UserConvert userConvert; + + @Autowired + private TeamConvert teamConvert; + + @Autowired + private UserAvatarMapper userAvatarMapper; + + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private UserRoleMapper userRoleMapper; + + @Autowired + private EmailEventPublisher publisher; + + @Autowired + private AuthServiceClient authServiceClient; + + @Autowired + private PermissionMapper permissionMapper; + + /** + * 测试标识 true:允许debug false:拒绝debug + */ + @Value("${debug.flag}") + private Boolean debugFlag; + + private final String LOCK_SEND_CODE = "LOCK_SEND_CODE"; + + /** + * 分页查询用户列表 + * + * @param criteria 查询条件 + * @param page 分页请求实体 + * @return java.lang.Object 用户列表返回实例 + */ + @Override + public Object queryAll(UserQueryDTO criteria, Page page) { + if (criteria.getRoleId() == null) { + IPage users = userMapper.selectCollPage(page, WrapperHelp.getWrapper(criteria)); + return PageUtil.toPage(users, userConvert::toDto); + } else { + IPage users = userMapper.selectCollPageByRoleId(page, WrapperHelp.getWrapper(criteria), criteria.getRoleId()); + return PageUtil.toPage(users, userConvert::toDto); + } + } + + /** + * 查询用户列表 + * + * @param criteria 用户查询条件 + * @return java.util.List 用户列表返回实例 + */ + @Override + public List queryAll(UserQueryDTO criteria) { + List users = userMapper.selectCollList(WrapperHelp.getWrapper(criteria)); + return userConvert.toDto(users); + } + + /** + * 根据用户ID查询团队列表 + * + * @param userId 用户ID + * @return java.util.List 团队列表信息 + */ + @Override + public List queryTeams(Long userId) { + + User user = userMapper.selectOne( + new LambdaQueryWrapper() + .eq(User::getId, userId) + .eq(User::getDeleted, SwitchEnum.getBooleanValue(SwitchEnum.OFF.getValue())) + ); + List teamList = teamMapper.findByUserId(user.getId()); + return teamConvert.toDto(teamList); + } + + /** + * 根据ID获取用户信息 + * + * @param id id + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + @Override + public UserDTO findById(long id) { + User user = userMapper.selectCollById(id); + return userConvert.toDto(user); + } + + + /** + * 新增用户 + * + * @param resources 用户新增实体 + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public UserDTO create(UserCreateDTO resources) { + PasswordEncoder passwordEncoder = PasswordEncoderFactory.getPasswordEncoder(); + if (!Objects.isNull(userMapper.findByUsername(resources.getUsername()))) { + throw new BusinessException("用户名已存在"); + } + if (userMapper.findByEmail(resources.getEmail()) != null) { + throw new BusinessException("邮箱已存在"); + } + resources.setPassword(passwordEncoder.encode(initialPassword)); + + User user = User.builder().build(); + BeanUtils.copyProperties(resources, user); + + userMapper.insert(user); + for (Role role : resources.getRoles()) { + roleMapper.tiedUserRole(user.getId(), role.getId()); + } + + return userConvert.toDto(user); + } + + + /** + * 修改用户 + * + * @param resources 用户修改请求实例 + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public UserDTO update(UserUpdateDTO resources) { + + //修改管理员信息校验 + checkIsAdmin(resources.getId()); + + User user = userMapper.selectCollById(resources.getId()); + User userTmp = userMapper.findByUsername(resources.getUsername()); + if (userTmp != null && !user.equals(userTmp)) { + throw new BusinessException("用户名已存在"); + } + userTmp = userMapper.findByEmail(resources.getEmail()); + if (userTmp != null && !user.equals(userTmp)) { + throw new BusinessException("邮箱已存在"); + } + roleMapper.untiedUserRoleByUserId(user.getId()); + for (Role role : resources.getRoles()) { + roleMapper.tiedUserRole(user.getId(), role.getId()); + } + user.setUsername(resources.getUsername()); + user.setEmail(resources.getEmail()); + user.setEnabled(resources.getEnabled()); + user.setRoles(resources.getRoles()); + user.setPhone(resources.getPhone()); + user.setNickName(resources.getNickName()); + user.setRemark(resources.getRemark()); + user.setSex(resources.getSex()); + userMapper.updateById(user); + return userConvert.toDto(user); + } + + + /** + * 批量删除用户信息 + * + * @param ids 用户ID列表 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + if (!CollectionUtils.isEmpty(ids)) { + Long adminId = Long.valueOf(UserConstant.ADMIN_USER_ID); + if (ids.contains(adminId)) { + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_CANNOT_DELETE); + } + ids.forEach(id -> { + userMapper.updateById( + User.builder() + .id(id) + .deleted(SwitchEnum.getBooleanValue(SwitchEnum.ON.getValue())) + .build()); + }); + } + } + + + /** + * 根据用户名称获取用户信息 + * + * @param userName 用户名称 + * @return org.dubhe.domain.dto.UserDTO 用户信息返回实例 + */ + @Override + public UserDTO findByName(String userName) { + User user = userMapper.findByUsername(userName); + if (user == null) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl findByName user is null"); + throw new BusinessException("user not found"); + } + UserDTO dto = new UserDTO(); + BeanUtils.copyProperties(user, dto); + List roles = roleMapper.findRolesByUserId(user.getId()); + if (!CollectionUtils.isEmpty(roles)) { + dto.setRoles(roles.stream().map(a -> { + SysRoleDTO sysRoleDTO = new SysRoleDTO(); + sysRoleDTO.setId(a.getId()); + sysRoleDTO.setName(a.getName()); + return sysRoleDTO; + }).collect(Collectors.toList())); + } + return dto; + + } + + + /** + * 修改用户个人中心信息 + * + * @param resources 个人用户信息修改请求实例 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCenter(UserCenterUpdateDTO resources) { + User user = userMapper.selectOne( + new LambdaQueryWrapper() + .eq(User::getId, resources.getId()) + .eq(User::getDeleted, SwitchEnum.getBooleanValue(SwitchEnum.OFF.getValue())) + ); + user.setNickName(resources.getNickName()); + user.setRemark(resources.getRemark()); + user.setPhone(resources.getPhone()); + user.setSex(resources.getSex()); + userMapper.updateById(user); + if (user.getUserAvatar() != null) { + if (user.getUserAvatar().getId() != null) { + userAvatarMapper.updateById(user.getUserAvatar()); + } else { + userAvatarMapper.insert(user.getUserAvatar()); + } + } + } + + + /** + * 修改用户密码 + * + * @param username 账号 + * @param pass 密码 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updatePass(String username, String pass) { + userMapper.updatePass(username, pass, new Date()); + } + + + /** + * 修改用户头像 + * + * @param realName 名称 + * @param path 头像路径 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAvatar(String realName, String path) { + User user = userMapper.findByUsername(JwtUtils.getCurUser().getUsername()); + UserAvatar userAvatar = user.getUserAvatar(); + UserAvatar newAvatar = new UserAvatar(userAvatar, realName, path, null); + + if (newAvatar.getId() != null) { + userAvatarMapper.updateById(newAvatar); + } else { + userAvatarMapper.insert(newAvatar); + } + user.setAvatarId(newAvatar.getId()); + user.setUserAvatar(newAvatar); + userMapper.updateById(user); + } + + /** + * 修改用户邮箱 + * + * @param username 用户名称 + * @param email 用户邮箱 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateEmail(String username, String email) { + userMapper.updateEmail(username, email); + } + + + /** + * 用户信息导出 + * + * @param queryAll 用户信息列表 + * @param response 导出http响应 + */ + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (UserDTO userDTO : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("用户名", userDTO.getUsername()); + map.put("邮箱", userDTO.getEmail()); + map.put("状态", userDTO.getEnabled() ? "启用" : "禁用"); + map.put("手机号码", userDTO.getPhone()); + map.put("最后修改密码的时间", userDTO.getLastPasswordResetTime()); + map.put("创建日期", userDTO.getCreateTime()); + list.add(map); + } + DubheFileUtil.downloadExcel(list, response); + } + + + /** + * 查询用户ID权限 + * + * @param id 用户ID + * @return java.util.Set 权限列表 + */ + @Override + public Set queryPermissionByUserId(Long id) { + return userMapper.queryPermissionByUserId(id); + } + + /** + * 用户注册信息 + * + * @param userRegisterDTO 用户注册请求实体 + * @return org.dubhe.base.DataResponseBody 注册返回结果集 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public DataResponseBody userRegister(UserRegisterDTO userRegisterDTO) { + PasswordEncoder passwordEncoder = PasswordEncoderFactory.getPasswordEncoder(); + //用户信息校验 + checkoutUserInfo(userRegisterDTO); + String encode = passwordEncoder.encode(RsaEncrypt.decrypt(userRegisterDTO.getPassword(), privateKey)); + try { + User newUser = User.builder() + .email(userRegisterDTO.getEmail()) + .enabled(true) + .nickName(userRegisterDTO.getNickName()) + .password(encode) + .phone(userRegisterDTO.getPhone()) + .sex(SwitchEnum.ON.getValue().compareTo(userRegisterDTO.getSex()) == 0 ? UserConstant.SEX_MEN : UserConstant.SEX_WOMEN) + .username(userRegisterDTO.getUsername()).build(); + + //新增用户注册信息 + userMapper.insert(newUser); + + //绑定用户默认权限 + userRoleMapper.insert(UserRole.builder().roleId((long) UserConstant.REGISTER_ROLE_ID).userId(newUser.getId()).build()); + + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl userRegister error , param:{} error:{}", JSONObject.toJSONString(userRegisterDTO), e); + throw new BusinessException(BaseErrorCodeEnum.ERROR_SYSTEM.getCode(), BaseErrorCodeEnum.ERROR_SYSTEM.getMsg()); + } + + return new DataResponseBody(); + } + + + /** + * 获取code通过发送邮件 + * + * @param userRegisterMailDTO 用户发送邮件请求实体 + * @return org.dubhe.base.DataResponseBody 发送邮件返回结果集 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public DataResponseBody getCodeBySentEmail(UserRegisterMailDTO userRegisterMailDTO) { + String email = userRegisterMailDTO.getEmail(); + + User dbUser = userMapper.selectOne(new LambdaQueryWrapper() + .eq(User::getEmail, email) + .eq(User::getDeleted, SwitchEnum.getBooleanValue(SwitchEnum.OFF.getValue())) + ); + //校验用户是否注册(type : 1 用户注册 2 修改邮箱 ) + Boolean isRegisterOrUpdate = UserMailCodeEnum.REGISTER_CODE.getValue().compareTo(userRegisterMailDTO.getType()) == 0 || + UserMailCodeEnum.MAIL_UPDATE_CODE.getValue().compareTo(userRegisterMailDTO.getType()) == 0; + if (!Objects.isNull(dbUser) && isRegisterOrUpdate) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl dbUser already register , dbUser:{} ", JSONObject.toJSONString(dbUser)); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_EMAIL_ALREADY_EXISTS.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_EMAIL_ALREADY_EXISTS.getMsg()); + } + + + //限制邮箱发送次数 + limitSendEmail(email); + + try { + synchronized (LOCK_SEND_CODE) { + //产生随机的验证码 + String code = RandomUtil.randomCode(); + //异步发送邮件 + publisher.sentEmailEvent( + EmailDTO.builder() + .code(code) + .subject(UserMailCodeEnum.getEnumValue(userRegisterMailDTO.getType()).getDesc()) + .type(userRegisterMailDTO.getType()) + .receiverMailAddress(email).build()); + //redis存储邮箱验证信息 + redisUtils.hset( + getSendEmailCodeRedisKeyByType(userRegisterMailDTO.getType()).concat(email), + email, + EmailVo.builder().code(code).email(email).build(), + UserConstant.DATE_SECOND); + } + + } catch (Exception e) { + redisUtils.hdel(UserConstant.USER_EMAIL_REGISTER.concat(email), email); + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl getCodeBySentEmail error , param:{} error:{}", email, e); + throw new BusinessException(BaseErrorCodeEnum.ERROR_SYSTEM.getCode(), BaseErrorCodeEnum.ERROR_SYSTEM.getMsg()); + } + return new DataResponseBody(); + } + + + /** + * 邮箱修改 + * + * @param userEmailUpdateDTO 修改邮箱请求实体 + * @return org.dubhe.base.DataResponseBody 修改邮箱返回结果集 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public DataResponseBody resetEmail(UserEmailUpdateDTO userEmailUpdateDTO) { + //校验邮箱信息 + User dbUser = checkoutEmailInfoByReset(userEmailUpdateDTO); + + try { + //修改邮箱信息 + userMapper.updateById( + User.builder() + .id(dbUser.getId()) + .email(userEmailUpdateDTO.getEmail()).build()); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl update email error , email:{} error:{}", userEmailUpdateDTO.getEmail(), e); + throw new BusinessException(BaseErrorCodeEnum.ERROR_SYSTEM.getCode(), + BaseErrorCodeEnum.ERROR_SYSTEM.getMsg()); + } + + return new DataResponseBody(); + } + + + /** + * 获取用户信息 + * + * @return java.util.Map 用户信息结果集 + */ + @Override + public Map userinfo() { + JwtUserDTO curUser = JwtUtils.getCurUser(); + if (Objects.isNull(curUser)) { + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_IS_NOT_EXISTS.getCode() + , BaseErrorCodeEnum.SYSTEM_USER_IS_NOT_EXISTS.getMsg()); + } + + //查询用户是否是管理员 + List userRoles = userRoleMapper.selectList( + new LambdaQueryWrapper() + .eq(UserRole::getUserId, curUser.getCurUserId()) + .eq(UserRole::getRoleId, Long.parseLong(String.valueOf(UserConstant.ADMIN_ROLE_ID))) + ); + UserVO vo = UserVO.builder() + .email(curUser.getUser().getEmail()) + .password(Md5Util.createMd5(Md5Util.createMd5(curUser.getUsername()).concat(initialPassword))) + .username(curUser.getUsername()) + .is_staff(!CollectionUtils.isEmpty(userRoles) ? true : false).build(); + + return BeanMap.create(vo); + } + + + /** + * 密码重置接口 + * + * @param userResetPasswordDTO 密码修改请求参数 + * @return org.dubhe.base.DataResponseBody 密码修改结果集 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public DataResponseBody resetPassword(UserResetPasswordDTO userResetPasswordDTO) { + PasswordEncoder passwordEncoder = PasswordEncoderFactory.getPasswordEncoder(); + //校验 邮箱地址 和 验证码 + checkoutEmailAndCode(userResetPasswordDTO.getCode(), userResetPasswordDTO.getEmail(), UserConstant.USER_EMAIL_RESET_PASSWORD); + + User dbUser = userMapper.selectOne(new LambdaQueryWrapper() + .eq(User::getEmail, userResetPasswordDTO.getEmail()) + .eq(User::getDeleted, SwitchEnum.getBooleanValue(SwitchEnum.OFF.getValue())) + ); + if (Objects.isNull(dbUser)) { + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_EMAIL_NOT_EXISTS.getCode() + , BaseErrorCodeEnum.SYSTEM_USER_EMAIL_NOT_EXISTS.getMsg()); + } + + //加密密码 + String encode = passwordEncoder.encode(RsaEncrypt.decrypt(userResetPasswordDTO.getPassword(), privateKey)); + try { + userMapper.updateById(User.builder().id(dbUser.getId()).password(encode).build()); + } catch (Exception e) { + throw new BusinessException(BaseErrorCodeEnum.ERROR_SYSTEM.getCode() + , BaseErrorCodeEnum.ERROR_SYSTEM.getMsg()); + } + return new DataResponseBody(); + } + + + /** + * 登录 + * + * @param authUserDTO 登录请求实体 + */ + @Override + @DataPermissionMethod + public DataResponseBody> login(AuthUserDTO authUserDTO) { + if (!debugFlag) { + validateCode(authUserDTO.getCode(), authUserDTO.getUuid()); + } + String password = null; + try { + RSA rsa = new RSA(privateKey, null); + password = new String(rsa.decrypt(authUserDTO.getPassword(), KeyType.PrivateKey)); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_SYS, "rsa 密钥解析失败, originPassword:{} , 密钥:{},异常:{}", authUserDTO.getPassword(), KeyType.PrivateKey, e); + throw new BusinessException("请输入正确密码"); + } + + Map params = new HashMap<>(); + params.put("grant_type", "password"); + params.put("username", authUserDTO.getUsername()); + params.put("client_id", AuthConst.CLIENT_ID); + params.put("client_secret", AuthConst.CLIENT_SECRET); + params.put("password", password); + params.put("scope", "all"); + DataResponseBody restResult = authServiceClient.postAccessToken(params); + Map authInfo = new HashMap<>(3); + if (ResponseCode.SUCCESS.compareTo(restResult.getCode()) == 0 && !Objects.isNull(restResult.getData())) { + Oauth2TokenDTO userDto = restResult.getData(); + UserDTO user = findByName(authUserDTO.getUsername()); + Set permissions = this.queryPermissionByUserId(user.getId()); + // 返回 token 与 用户信息 + authInfo.put("token", userDto.getTokenHead() + userDto.getToken()); + authInfo.put("user", user); + authInfo.put("permissions", permissions); + } + return DataResponseFactory.success(authInfo); + } + + + /** + * 退出登录 + * + * @param accessToken token + */ + @Override + public DataResponseBody logout(String accessToken) { + return authServiceClient.logout(accessToken); + } + + /** + * 根据用户昵称获取用户信息 + * + * @param nickName 用户昵称 + * @return org.dubhe.domain.dto.UserDTO 用户信息DTO + */ + @Override + public List findByNickName(String nickName) { + List users = userMapper.selectList(new LambdaQueryWrapper() + .like(User::getNickName, nickName == null ? StrUtil.EMPTY : nickName)); + + return userConvert.toDto(users); + } + + /** + * 根据用户id批量查询用户信息 + * + * @param ids 用户id集合 + * @return org.dubhe.domain.dto.UserDTO 用户信息DTO集合 + */ + @Override + public List getUserList(List ids) { + List users = userMapper.selectBatchIds(ids); + return userConvert.toDto(users); + } + + + /** + * 校验验证码 + * + * @param loginCaptcha 验证码参数 + * @param uuid 验证码redis-key + */ + private void validateCode(String loginCaptcha, String uuid) { + // 验证码未输入 + if (loginCaptcha == null || "".equals(loginCaptcha)) { + throw new CaptchaException("验证码错误"); + } + String sessionCaptcha = (String) redisUtils.get(uuid); + + if (!loginCaptcha.equalsIgnoreCase(sessionCaptcha)) { + throw new CaptchaException("验证码错误"); + } + + } + + /** + * 修改邮箱校验邮箱信息 + * + * @param userEmailUpdateDTO 邮箱修改参数校验实体 + */ + private User checkoutEmailInfoByReset(UserEmailUpdateDTO userEmailUpdateDTO) { + PasswordEncoder passwordEncoder = PasswordEncoderFactory.getPasswordEncoder(); + String email = userEmailUpdateDTO.getEmail(); + //管理员信息校验 + checkIsAdmin(userEmailUpdateDTO.getUserId()); + + //校验用户信息是否存在 + User dbUser = userMapper.selectCollById(userEmailUpdateDTO.getUserId()); + if (ObjectUtil.isNull(dbUser)) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl dbUser is null , userId:{}", userEmailUpdateDTO.getUserId()); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_IS_NOT_EXISTS.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_IS_NOT_EXISTS.getMsg()); + } + //校验密码是否正确 + String decryptPassword = RsaEncrypt.decrypt(userEmailUpdateDTO.getPassword(), privateKey); + if (!passwordEncoder.matches(decryptPassword, dbUser.getPassword())) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl password error , webPassword:{}, dbPassword:{} ", + userEmailUpdateDTO.getPassword(), dbUser.getPassword()); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_EMAIL_PASSWORD_ERROR.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_EMAIL_PASSWORD_ERROR.getMsg()); + } + + //邮箱唯一性校验 + User user = userMapper.selectOne(new LambdaQueryWrapper() + .eq(User::getEmail, userEmailUpdateDTO.getEmail())); + if (!ObjectUtil.isNull(user)) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl Email already exists , email:{} ", userEmailUpdateDTO.getEmail()); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_EMAIL_ALREADY_EXISTS.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_EMAIL_ALREADY_EXISTS.getMsg()); + } + + //校验 邮箱地址 和 验证码 + checkoutEmailAndCode(userEmailUpdateDTO.getCode(), email, UserConstant.USER_EMAIL_UPDATE); + + return dbUser; + } + + + /** + * 限制发送次数 + * + * @param receiverMailAddress 邮箱接受者地址 + */ + private void limitSendEmail(final String receiverMailAddress) { + double count = redisUtils.hincr(UserConstant.USER_EMAIL_LIMIT_COUNT.concat(receiverMailAddress), receiverMailAddress, 1); + if (count > UserConstant.COUNT_SENT_EMAIL) { + LogUtil.error(LogEnum.SYS_ERR, "Email verification code cannot exceed three times , error:{}", UserConstant.COUNT_SENT_EMAIL); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_EMAIL_CODE_CANNOT_EXCEED_TIMES.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_EMAIL_CODE_CANNOT_EXCEED_TIMES.getMsg()); + } else { + // 验证码次数凌晨清除 + String concat = UserConstant.USER_EMAIL_LIMIT_COUNT.concat(receiverMailAddress); + + Long secondsNextEarlyMorning = DateUtil.getSecondTime(); + + redisUtils.expire(concat, secondsNextEarlyMorning); + } + } + + + /** + * 用户信息校验 + * + * @param userRegisterDTO 用户信息校验实体 + */ + private void checkoutUserInfo(UserRegisterDTO userRegisterDTO) { + //账户唯一性校验 + User user = userMapper.selectOne(new LambdaQueryWrapper() + .eq(User::getUsername, userRegisterDTO.getUsername())); + if (!ObjectUtil.isNull(user)) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl username already exists , username:{} ", userRegisterDTO.getUsername()); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USERNAME_ALREADY_EXISTS.getCode(), + BaseErrorCodeEnum.SYSTEM_USERNAME_ALREADY_EXISTS.getMsg()); + } + //校验 邮箱地址 和 验证码 + checkoutEmailAndCode(userRegisterDTO.getCode(), userRegisterDTO.getEmail(), UserConstant.USER_EMAIL_REGISTER); + } + + /** + * 校验 邮箱地址 和 验证码 + * + * @param code 验证码 + * @param email 邮箱 + * @param codeRedisKey redis-key + */ + private void checkoutEmailAndCode(String code, String email, String codeRedisKey) { + //校验验证码是否过期 + Object emailVoObj = redisUtils.hget(codeRedisKey.concat(email), email); + if (Objects.isNull(emailVoObj)) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl emailVo already expired , email:{} ", email); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_REGISTER_EMAIL_INFO_EXPIRED.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_REGISTER_EMAIL_INFO_EXPIRED.getMsg()); + } + + //校验邮箱和验证码 + EmailVo emailVo = (EmailVo) emailVoObj; + if (!email.equals(emailVo.getEmail()) || !code.equals(emailVo.getCode())) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl email or code error , email:{} code:{}", email, code); + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_EMAIL_OR_CODE_ERROR.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_EMAIL_OR_CODE_ERROR.getMsg()); + } + } + + + /** + * 获取 发送邮箱code 的 redis key + * + * @param type 发送邮件类型 + */ + private String getSendEmailCodeRedisKeyByType(Integer type) { + String typeKey = null; + if (UserMailCodeEnum.REGISTER_CODE.getValue().compareTo(type) == 0) { + typeKey = UserConstant.USER_EMAIL_REGISTER; + } else if (UserMailCodeEnum.MAIL_UPDATE_CODE.getValue().compareTo(type) == 0) { + typeKey = UserConstant.USER_EMAIL_UPDATE; + } else if (UserMailCodeEnum.FORGET_PASSWORD.getValue().compareTo(type) == 0) { + typeKey = UserConstant.USER_EMAIL_RESET_PASSWORD; + } else { + typeKey = UserConstant.USER_EMAIL_OTHER; + } + return typeKey; + } + + + /** + * 修改管理员信息校验 + * + * @param userId 用户ID + */ + private void checkIsAdmin(Long userId) { + //修改管理员信息校验 + if (UserConstant.ADMIN_USER_ID == userId.intValue() && + UserConstant.ADMIN_USER_ID != JwtUtils.getCurUserId().intValue()) { + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_CANNOT_UPDATE_ADMIN); + } + } + + + /** + * 根据用户名查询用户信息 + * + * @param username 用户名称 + * @return 用户信息 + */ + @Override + public DataResponseBody findUserByUsername(String username) { + User user = userMapper.findByUsername(username); + if (Objects.isNull(user)) { + LogUtil.error(LogEnum.SYS_ERR, "UserServiceImpl findUserByUsername user is null {}"); + throw new BusinessException("用户信息不存在!"); + } + UserContext dto = new UserContext(); + BeanUtils.copyProperties(user, dto); + if (user.getUserAvatar() != null && user.getUserAvatar().getPath() != null) { + dto.setUserAvatarPath(user.getUserAvatar().getPath()); + } + List roles = roleMapper.selectRoleByUserId(user.getId()); + if (!CollectionUtils.isEmpty(roles)) { + + List roleIds = roles.stream().map(a -> a.getId()).collect(Collectors.toList()); + //获取菜单权限 + List permissions = menuMapper.selectPermissionByRoleIds(roleIds); + //获取操作权限 + List authList = permissionMapper.selectPermissinByRoleIds(roleIds); + permissions.addAll(authList); + Map> permissionMap = new HashMap<>(permissions.size()); + if (!CollectionUtils.isEmpty(permissions)) { + permissionMap = permissions.stream().collect(Collectors.groupingBy(SysPermissionDTO::getRoleId)); + } + + Map> finalPermissionMap = permissionMap; + List roleDTOS = roles.stream().map(a -> { + SysRoleDTO sysRoleDTO = new SysRoleDTO(); + BeanUtils.copyProperties(a, sysRoleDTO); + List sysPermissionDTOS = finalPermissionMap.get(a.getId()); + sysRoleDTO.setPermissions(sysPermissionDTOS); + return sysRoleDTO; + }).collect(Collectors.toList()); + dto.setRoles(roleDTOS); + } + + + return DataResponseFactory.success(dto); + } +} diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleInvalidResourcesTask.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleInvalidResourcesTask.java new file mode 100644 index 0000000..dea14cd --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleInvalidResourcesTask.java @@ -0,0 +1,56 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.task; + +import org.dubhe.admin.service.RecycleTaskService; +import org.dubhe.biz.file.config.NfsConfig; +import org.dubhe.biz.log.handler.ScheduleTaskHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.io.File; + +/** + * @description 回收无效文件资源定时任务 + * @date 2020-09-21 + */ +@Component +public class RecycleInvalidResourcesTask { + + @Autowired + private NfsConfig nfsConfig; + + @Autowired + private RecycleTaskService recycleTaskService; + + /** + * 文件存储临时文件根目录 + */ + public static final String UPLOAD_TEMP = File.separator + "upload-temp"; + + /** + * 每天晚上12点定时回收无效文件资源 + */ + @Scheduled(cron = "0 0 0 * * ?") + public void process() { + ScheduleTaskHandler.process(() -> { + String sourcePath = nfsConfig.getRootDir() + nfsConfig.getBucket() + UPLOAD_TEMP; + recycleTaskService.deleteInvalidResourcesByCMD(sourcePath); + }); + } +} \ No newline at end of file diff --git a/dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleResourcesTask.java b/dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleResourcesTask.java new file mode 100644 index 0000000..442a305 --- /dev/null +++ b/dubhe-server/admin/src/main/java/org/dubhe/admin/task/RecycleResourcesTask.java @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.admin.task; + +import org.dubhe.admin.service.RecycleTaskService; +import org.dubhe.biz.base.constant.UserConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.handler.ScheduleTaskHandler; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.recycle.domain.entity.Recycle; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @description 回收资源定时任务 + * @date 2020-09-21 + */ +@Component +public class RecycleResourcesTask { + + @Autowired + private RecycleTaskService recycleTaskService; + + /** + * 每天凌晨1点定时回收已删除资源 + */ + @Scheduled(cron = "0 0 1 * * ?") + public void process() { + ScheduleTaskHandler.process(() -> { + List recycleTaskList = recycleTaskService.getRecycleTaskList(); + for (Recycle recycle : recycleTaskList) { + // one by one + try { + recycleTaskService.recycleTask(recycle, UserConstant.ADMIN_USER_ID); + } catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "scheduled recycle task failed,exception {}", e); + } + } + }); + } + +} diff --git a/dubhe-server/admin/src/main/resources/banner.txt b/dubhe-server/admin/src/main/resources/banner.txt new file mode 100644 index 0000000..ef44bd8 --- /dev/null +++ b/dubhe-server/admin/src/main/resources/banner.txt @@ -0,0 +1,14 @@ + + + + _____ _ _ _ _ + | __ \ | | | | /\ | | (_) + | | | |_ _| |__ | |__ ___ / \ __| |_ __ ___ _ _ __ + | | | | | | | '_ \| '_ \ / _ \ / /\ \ / _` | '_ ` _ \| | '_ \ + | |__| | |_| | |_) | | | | __/ / ____ \ (_| | | | | | | | | | | + |_____/ \__,_|_.__/|_| |_|\___| /_/ \_\__,_|_| |_| |_|_|_| |_| + + + + + :: Dubhe Admin :: 0.0.1-SNAPSHOT diff --git a/dubhe-server/admin/src/main/resources/bootstrap.yml b/dubhe-server/admin/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..e5b0ebc --- /dev/null +++ b/dubhe-server/admin/src/main/resources/bootstrap.yml @@ -0,0 +1,37 @@ +server: + port: 8870 + +spring: + application: + name: admin + profiles: + active: dev + cloud: + nacos: + config: + enabled: true + server-addr: 127.0.0.1:8848 + namespace: dubhe-server-cloud-prod + shared-configs[0]: + data-id: common-biz.yaml + group: dubhe + refresh: true # 是否动态刷新,默认为false + shared-configs[1]: + # 配置1 + data-id: common-k8s.yaml + group: dubhe + refresh: true + shared-configs[2]: + data-id: common-recycle.yaml + group: dubhe + refresh: true + shared-configs[3]: + data-id: admin.yaml + group: dubhe + refresh: true + discovery: + enabled: true + namespace: dubhe-server-cloud-prod + group: dubhe + server-addr: 127.0.0.1:8848 + diff --git a/dubhe-server/admin/src/main/resources/mapper/AuthMapper.xml b/dubhe-server/admin/src/main/resources/mapper/AuthMapper.xml new file mode 100644 index 0000000..af66699 --- /dev/null +++ b/dubhe-server/admin/src/main/resources/mapper/AuthMapper.xml @@ -0,0 +1,18 @@ + + + + + + insert into roles_auth(role_id,auth_id)values + + (#{item.roleId},#{item.authId}) + + + + + insert into auth_permission(auth_id,permission_id)values + + (#{item.authId},#{item.permissionId}) + + + \ No newline at end of file diff --git a/dubhe-server/admin/src/main/resources/mapper/MenuMapper.xml b/dubhe-server/admin/src/main/resources/mapper/MenuMapper.xml new file mode 100644 index 0000000..deac611 --- /dev/null +++ b/dubhe-server/admin/src/main/resources/mapper/MenuMapper.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/dubhe-server/admin/src/main/resources/mapper/PermissionMapper.xml b/dubhe-server/admin/src/main/resources/mapper/PermissionMapper.xml new file mode 100644 index 0000000..2729dd2 --- /dev/null +++ b/dubhe-server/admin/src/main/resources/mapper/PermissionMapper.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/dubhe-server/admin/src/main/resources/mapper/UserMapper.xml b/dubhe-server/admin/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..7bd66ca --- /dev/null +++ b/dubhe-server/admin/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/dubhe-server/admin/src/main/resources/mapper/UserRoleMapper.xml b/dubhe-server/admin/src/main/resources/mapper/UserRoleMapper.xml new file mode 100644 index 0000000..95f5062 --- /dev/null +++ b/dubhe-server/admin/src/main/resources/mapper/UserRoleMapper.xml @@ -0,0 +1,19 @@ + + + + + + + delete from users_roles where user_id in + + #{item} + + + + + insert into users_roles (user_id,role_id)values + + (#{item.userId}, #{item.roleId}) + + + \ No newline at end of file diff --git a/dubhe-server/admin/src/test/java/org/dubhe/admin/AdminApplicationTests.java b/dubhe-server/admin/src/test/java/org/dubhe/admin/AdminApplicationTests.java new file mode 100644 index 0000000..b145a05 --- /dev/null +++ b/dubhe-server/admin/src/test/java/org/dubhe/admin/AdminApplicationTests.java @@ -0,0 +1,70 @@ +package org.dubhe.admin; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.admin.domain.dto.DictCreateDTO; +import org.dubhe.admin.domain.dto.DictDetailDTO; +import org.dubhe.admin.domain.dto.DictDetailQueryDTO; +import org.dubhe.admin.domain.entity.DictDetail; +import org.dubhe.admin.rest.DictController; +import org.dubhe.admin.service.DictDetailService; +import org.dubhe.biz.base.utils.DateUtil; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description Admin启动类单元测试 + * @date: 2020-12-02 + */ +@SpringBootTest +public class AdminApplicationTests { + + @Autowired + private DictController dictController; + @Autowired + private DictDetailService dictDetailService; + + /** + * 字典分页查询 + */ + @Test + public void demo01() { + + Page page = new Page<>(); + page.setCurrent(1); + page.setSize(5); + DictDetailQueryDTO dictQueryDTO = new DictDetailQueryDTO(); + dictQueryDTO.setDictId(3L); + List dictDetailDTOS = dictDetailService.queryAll(dictQueryDTO); + System.out.println(dictDetailDTOS.size()); + + } + + /** + * 字典创建 + */ + @Test + public void demo02() { + DictCreateDTO dict = new DictCreateDTO(); + dict.setName("测试"); + dict.setCreateTime(DateUtil.getCurrentTimestamp()); + dict.setRemark("观点"); + List dictDetails = new ArrayList<>(); + DictDetail dictDetail = new DictDetail(); + dictDetail.setCreateTime(DateUtil.getCurrentTimestamp()); + dictDetail.setId(2222L); + dictDetail.setLabel("开发"); + dictDetail.setSort("9"); + dictDetail.setValue("1"); + dictDetails.add(dictDetail); + dict.setDictDetails(dictDetails); + DataResponseBody dataResponseBody = dictController.create(dict); + System.out.println(dataResponseBody.getData()); + + } + +} diff --git a/dubhe-server/admin/src/test/java/org/dubhe/admin/DictControllerTest.java b/dubhe-server/admin/src/test/java/org/dubhe/admin/DictControllerTest.java new file mode 100644 index 0000000..6a3ae9c --- /dev/null +++ b/dubhe-server/admin/src/test/java/org/dubhe/admin/DictControllerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.admin; + +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.cloud.unittest.base.BaseTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +/** + * @description 实体类 + * @date 2020-03-25 + */ +@RunWith(SpringRunner.class) +@SpringBootTest +public class DictControllerTest extends BaseTest { + + @Test + public void whenQueryUserAll() throws Exception { + String accessToken = obtainAccessToken(); + mockMvcWithNoRequestBody(mockMvc.perform(MockMvcRequestBuilders.get("/api/dict/all").header(AuthConst.AUTHORIZATION, AuthConst.ACCESS_TOKEN_PREFIX + accessToken)) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse(), 200); + } +} diff --git a/dubhe-server/auth/pom.xml b/dubhe-server/auth/pom.xml new file mode 100644 index 0000000..81fee47 --- /dev/null +++ b/dubhe-server/auth/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.dubhe + server + 0.0.1-SNAPSHOT + + auth + 0.0.1-SNAPSHOT + 授权中心 + Dubhe Authentication + + + + + org.dubhe.biz + data-response + ${org.dubhe.biz.data-response.version} + + + + org.dubhe.cloud + auth-config + ${org.dubhe.cloud.auth-config.version} + + + + org.dubhe.cloud + remote-call + ${org.dubhe.cloud.remote-call.version} + + + + org.dubhe.cloud + registration + ${org.dubhe.cloud.registration.version} + + + + org.dubhe.cloud + configuration + ${org.dubhe.cloud.configuration.version} + + + + org.dubhe.cloud + swagger + ${org.dubhe.cloud.swagger.version} + + + + org.dubhe.biz + data-permission + ${org.dubhe.biz.data-permission.version} + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + org.springframework.boot + spring-boot-starter-web + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + true + exec + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/AuthApplication.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/AuthApplication.java new file mode 100644 index 0000000..a02a707 --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/AuthApplication.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.auth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @description Auth启动类 + * @date 2020-12-02 + */ +@SpringBootApplication(scanBasePackages = "org.dubhe") +public class AuthApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthApplication.class, args); + } + +} diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/config/AuthorizationServerConfig.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/config/AuthorizationServerConfig.java new file mode 100644 index 0000000..3ed70ab --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/config/AuthorizationServerConfig.java @@ -0,0 +1,100 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.auth.config; + +import org.dubhe.cloud.authconfig.factory.PasswordEncoderFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; +import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; + +import javax.sql.DataSource; + +/** + * @description 授权配置 + * @date 2020-11-05 + */ +@EnableAuthorizationServer +@Configuration +public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{ + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private DataSource dataSource; + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private JdbcTokenStore tokenStore; + + @Autowired + private JwtAccessTokenConverter accessTokenConverter; + + @Autowired + @Qualifier("customerOauthWebResponseExceptionTranslator") + private WebResponseExceptionTranslator webResponseExceptionTranslator; + + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + clients + .jdbc(dataSource) + .passwordEncoder(PasswordEncoderFactory.getPasswordEncoder()) + ; + } + + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) { + endpoints + .tokenStore(tokenStore) + .accessTokenConverter(accessTokenConverter) + .authenticationManager(authenticationManager) + // 刷新token必须设置userDetailsService + .userDetailsService(userDetailsService) + .exceptionTranslator(webResponseExceptionTranslator)//认证异常处理器 + // 重用刷新token + .reuseRefreshTokens(true); + } + + + /** + * 允许所有人请求令牌 + * 已验证的可客户端才能请求check_token端点 + * @param security + * @throws Exception + */ + @Override + public void configure(AuthorizationServerSecurityConfigurer security) { + security + .passwordEncoder(PasswordEncoderFactory.getPasswordEncoder()) + .tokenKeyAccess("permitAll()") + .checkTokenAccess("isAuthenticated()") + .allowFormAuthenticationForClients(); + } +} diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/config/SecurityConfig.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/config/SecurityConfig.java new file mode 100644 index 0000000..56abcf2 --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/config/SecurityConfig.java @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.auth.config; + +import org.dubhe.cloud.authconfig.factory.PasswordEncoderFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @description 安全配置 + * @date 2020-11-05 + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + + @Autowired + @Qualifier("authenticationProviderImpl") + private AuthenticationProvider authenticationProvider; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(authenticationProvider); + } + + @Bean + public PasswordEncoder passwordEncoder(){ + return PasswordEncoderFactory.getPasswordEncoder(); + } + + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + // 允许访问/oauth 授权接口 + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .and() + .requestMatchers().anyRequest() + .and() + .authorizeRequests() + .antMatchers("/oauth/*").permitAll() + // swagger + .antMatchers("/swagger**/**").permitAll() + .antMatchers("/webjars/**").permitAll() + .antMatchers("/v2/api-docs/**").permitAll() + // 其他所有请求需要身份认证 + .anyRequest().authenticated(); + + } + +} diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/AuthenticationProviderImpl.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/AuthenticationProviderImpl.java new file mode 100644 index 0000000..e47ef5d --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/AuthenticationProviderImpl.java @@ -0,0 +1,76 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.auth.exception; + +import org.apache.commons.lang3.StringUtils; +import org.dubhe.biz.base.exception.FeignException; +import org.dubhe.cloud.authconfig.dto.JwtUserDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Objects; + + +/** + * @description 抛出自定义中文错误信息 + * @date 2020-12-21 + */ +@Service +public class AuthenticationProviderImpl implements AuthenticationProvider { + @Autowired + @Qualifier("userDetailsServiceImpl") + private UserDetailsService userDetailsService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + String username = authentication.getName(); + String password = authentication.getCredentials().toString(); + JwtUserDTO userDTO; + try { + userDTO = (JwtUserDTO) userDetailsService.loadUserByUsername(username); + } catch (Exception e) { + if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().contains("Load balancer does not have available server for client")) { + throw new BadCredentialsException("用户服务不可用!", e); + } else if (e instanceof FeignException) { + throw new BadCredentialsException("用户服务" + ((FeignException) e).getResponseBody().getMsg(), e); + } else { + throw new BadCredentialsException("用户信息不存在!", e); + } + } + if (!Objects.isNull(userDTO.getUser()) && !userDTO.getUser().getEnabled()) { + throw new BadCredentialsException("帐号已锁定!"); + } + if (!new BCryptPasswordEncoder().matches(password, userDTO.getPassword())) { + throw new BadCredentialsException("密码错误!"); + } + return new UsernamePasswordAuthenticationToken(userDTO, userDTO.getPassword(), null); + } + + @Override + public boolean supports(Class aClass) { + return UsernamePasswordAuthenticationToken.class.equals(aClass); + } +} \ No newline at end of file diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthException.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthException.java new file mode 100644 index 0000000..c2d76bf --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthException.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.auth.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + * @description 定义自已的异常处理类 + * @date 2020-12-21 + */ +@JsonSerialize(using = CustomerOauthExceptionSerializer.class) +public class CustomerOauthException extends OAuth2Exception { + public CustomerOauthException(String msg, Throwable t) { + super(msg, t); + } + + public CustomerOauthException(String msg) { + super(msg); + } + +} diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthExceptionSerializer.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthExceptionSerializer.java new file mode 100644 index 0000000..c3ec451 --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthExceptionSerializer.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.auth.exception; + + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * @description 序列化异常处理类 + * @date 2020-12-21 + */ +public class CustomerOauthExceptionSerializer extends StdSerializer { + + protected CustomerOauthExceptionSerializer() { + super(CustomerOauthException.class); + } + + @Override + public void serialize(CustomerOauthException e, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("code", String.valueOf(e.getHttpErrorCode())); + jsonGenerator.writeStringField("msg", e.getMessage()); + jsonGenerator.writeStringField("data",null); + jsonGenerator.writeEndObject(); + } +} + diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthWebResponseExceptionTranslator.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthWebResponseExceptionTranslator.java new file mode 100644 index 0000000..f93e43e --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/exception/CustomerOauthWebResponseExceptionTranslator.java @@ -0,0 +1,168 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.auth.exception; + + + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; +import org.springframework.security.web.util.ThrowableAnalyzer; +import org.springframework.stereotype.Component; +import org.springframework.web.HttpRequestMethodNotSupportedException; + +import java.io.IOException; + + + +/** + * @description 异常捕获并通过CustomerOauthException处理 + * @date 2020-12-21 + */ +@Component +public class CustomerOauthWebResponseExceptionTranslator implements WebResponseExceptionTranslator { + private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); + + @Override + public ResponseEntity translate(Exception e) throws Exception { + Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e); + Exception ase=null; + + // 异常栈获取 OAuth2Exception 异常 + ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType( + OAuth2Exception.class, causeChain); + // 异常栈中有OAuth2Exception + if (ase != null) { + return handleOAuth2Exception((OAuth2Exception) ase); + } + + ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, + causeChain); + if (ase != null) { + return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e)); + } + + ase = (AccessDeniedException) throwableAnalyzer + .getFirstThrowableOfType(AccessDeniedException.class, causeChain); + if (ase instanceof AccessDeniedException) { + return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase)); + } + + ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer + .getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain); + if (ase instanceof HttpRequestMethodNotSupportedException) { + return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase)); + } + + // 不包含上述异常则服务器内部错误 + return handleOAuth2Exception(new ServerErrorException(HttpStatus.OK.getReasonPhrase(), e)); + } + + //使用自定义的异常处理类处理异常 + private ResponseEntity handleOAuth2Exception(OAuth2Exception e) throws IOException { + int status = e.getHttpErrorCode(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Cache-Control", "no-store"); + headers.set("Pragma", "no-cache"); + if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) { + headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary())); + } + CustomerOauthException exception = new CustomerOauthException(e.getMessage(), e); + + ResponseEntity response = new ResponseEntity(exception, headers, + HttpStatus.OK); + + return response; + + } + + @SuppressWarnings("serial") + private static class ForbiddenException extends OAuth2Exception { + + public ForbiddenException(String msg, Throwable t) { + super(msg, t); + } + + public String getOAuth2ErrorCode() { + return "access_denied"; + } + + public int getHttpErrorCode() { + return 403; + } + + } + + @SuppressWarnings("serial") + private static class ServerErrorException extends OAuth2Exception { + + public ServerErrorException(String msg, Throwable t) { + super(msg, t); + } + + public String getOAuth2ErrorCode() { + return "server_error"; + } + + public int getHttpErrorCode() { + return 500; + } + + } + + @SuppressWarnings("serial") + private static class UnauthorizedException extends OAuth2Exception { + + public UnauthorizedException(String msg, Throwable t) { + super(msg, t); + } + + public String getOAuth2ErrorCode() { + return "unauthorized"; + } + + public int getHttpErrorCode() { + return 401; + } + + } + + @SuppressWarnings("serial") + private static class MethodNotAllowed extends OAuth2Exception { + + public MethodNotAllowed(String msg, Throwable t) { + super(msg, t); + } + + public String getOAuth2ErrorCode() { + return "method_not_allowed"; + } + + public int getHttpErrorCode() { + return 405; + } + + } + +} diff --git a/dubhe-server/auth/src/main/java/org/dubhe/auth/rest/AuthController.java b/dubhe-server/auth/src/main/java/org/dubhe/auth/rest/AuthController.java new file mode 100644 index 0000000..a370fb0 --- /dev/null +++ b/dubhe-server/auth/src/main/java/org/dubhe/auth/rest/AuthController.java @@ -0,0 +1,118 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.auth.rest; + +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.dubhe.biz.base.dto.Oauth2TokenDTO; +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; +import org.springframework.security.oauth2.provider.token.ConsumerTokenServices; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +import javax.annotation.Resource; +import java.security.Principal; +import java.util.Map; + +/** + * @description 授权接口类 + * @date 2020-11-03 + */ +@RestController +@RequestMapping(value = "/oauth") +public class AuthController { + + @Resource + private ConsumerTokenServices consumerTokenServices; + + @Autowired + private UserContextService userContextService; + + @Autowired + private TokenEndpoint tokenEndpoint; + + @ApiOperation("Oauth2获取token") + @ApiImplicitParams({ + @ApiImplicitParam(name = "grant_type", value = "授权模式", required = true), + @ApiImplicitParam(name = "client_id", value = "Oauth2客户端ID", required = true), + @ApiImplicitParam(name = "client_secret", value = "Oauth2客户端秘钥", required = true), + @ApiImplicitParam(name = "refresh_token", value = "刷新token"), + @ApiImplicitParam(name = "username", value = "登录用户名"), + @ApiImplicitParam(name = "password", value = "登录密码") + }) + @RequestMapping(value = "/token", method = RequestMethod.POST) + public DataResponseBody postAccessToken(@ApiIgnore Principal principal, @ApiIgnore @RequestParam Map parameters) throws HttpRequestMethodNotSupportedException { + OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); + Oauth2TokenDTO oauth2TokenDto = Oauth2TokenDTO.builder() + .token(oAuth2AccessToken.getValue()) + .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) + .expiresIn(oAuth2AccessToken.getExpiresIn()) + .tokenHead(AuthConst.ACCESS_TOKEN_PREFIX).build(); + + return DataResponseFactory.success(oauth2TokenDto); + } + + + /** + * 基于authorization_code方式授权的回调接口 + * @param code + * @return + */ + @GetMapping(value="/callback") + public String hello(String code){ + return "Auth code -> ".concat(code); + } + + + /** + * 获取当前用户 + * @param principal + */ + @GetMapping(value="/user") + @ResponseBody + public UserContext getCurUser(Principal principal){ + return userContextService.getCurUser(); + } + + + + /** + * 自定义登出(请求时header还是需要Authorization信息) + * @param accessToken token + * @return + */ + @DeleteMapping(value="/logout") + public DataResponseBody logout(@RequestParam("token")String accessToken){ + String token = StringUtils.substringAfter(accessToken, "Bearer ").trim(); + if (consumerTokenServices.revokeToken(token)){ + // 登出成功,自定义登出业务逻辑 + return DataResponseFactory.success(); + } + return DataResponseFactory.failed("Logout failed!"); + } + +} diff --git a/dubhe-server/auth/src/main/resources/banner.txt b/dubhe-server/auth/src/main/resources/banner.txt new file mode 100644 index 0000000..c1b1ceb --- /dev/null +++ b/dubhe-server/auth/src/main/resources/banner.txt @@ -0,0 +1,13 @@ + + + _____ _ _ _ _ _ _ _ _ + | __ \ | | | | /\ | | | | | | (_) | | (_) + | | | |_ _| |__ | |__ ___ / \ _ _| |_| |__ ___ _ __ | |_ _ ___ __ _| |_ _ ___ _ __ + | | | | | | | '_ \| '_ \ / _ \ / /\ \| | | | __| '_ \ / _ \ '_ \| __| |/ __/ _` | __| |/ _ \| '_ \ + | |__| | |_| | |_) | | | | __/ / ____ \ |_| | |_| | | | __/ | | | |_| | (_| (_| | |_| | (_) | | | | + |_____/ \__,_|_.__/|_| |_|\___| /_/ \_\__,_|\__|_| |_|\___|_| |_|\__|_|\___\__,_|\__|_|\___/|_| |_| + + + + + :: Dubhe Authentication :: 0.0.1-SNAPSHOT diff --git a/dubhe-server/auth/src/main/resources/bootstrap.yml b/dubhe-server/auth/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..2e2179e --- /dev/null +++ b/dubhe-server/auth/src/main/resources/bootstrap.yml @@ -0,0 +1,29 @@ +server: + port: 8866 + + +spring: + application: + name: auth + profiles: + active: dev + cloud: + nacos: + config: + enabled: true + server-addr: 127.0.0.1:8848 + namespace: dubhe-server-cloud-prod + shared-configs[0]: + data-id: common-biz.yaml + group: dubhe + refresh: true # 是否动态刷新,默认为false + shared-configs[1]: + data-id: auth.yaml + group: dubhe + refresh: true + discovery: + enabled: true + namespace: dubhe-server-cloud-prod + group: dubhe + server-addr: 127.0.0.1:8848 + diff --git a/dubhe-server/auth/src/test/java/org/dubhe/auth/AuthApplicationTests.java b/dubhe-server/auth/src/test/java/org/dubhe/auth/AuthApplicationTests.java new file mode 100644 index 0000000..fe4ff09 --- /dev/null +++ b/dubhe-server/auth/src/test/java/org/dubhe/auth/AuthApplicationTests.java @@ -0,0 +1,13 @@ +package org.dubhe.auth; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/dubhe-server/common-biz/base/pom.xml b/dubhe-server/common-biz/base/pom.xml new file mode 100644 index 0000000..abc26cc --- /dev/null +++ b/dubhe-server/common-biz/base/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + org.dubhe.biz + common-biz + 0.0.1-SNAPSHOT + + base + 0.0.1-SNAPSHOT + Biz 通用配置 + Base config for Dubhe Server + + + + + org.projectlombok + lombok + ${lombok.version} + + + org.springframework + spring-web + + + javax.servlet + javax.servlet-api + ${javax.servlet-api.version} + + + + jakarta.validation + jakarta.validation-api + + + org.hibernate.validator + hibernate-validator + + + com.thoughtworks.qdox + qdox + ${com.thoughtworks.qdox.version} + + + org.projectlombok + lombok + + + + jakarta.validation + jakarta.validation-api + + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation-api.version} + + + + + commons-codec + commons-codec + + + + + + + + + + + diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/ApiVersion.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/ApiVersion.java new file mode 100644 index 0000000..5e516ce --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/ApiVersion.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.annotation; + +import java.lang.annotation.*; + +/** + * @description API版本控制注解 + * @date 2020-04-06 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ApiVersion { + //标识版本号 + int value() default 1; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/DataPermission.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/DataPermission.java new file mode 100644 index 0000000..54509a2 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/DataPermission.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.annotation; + +/** + * @description 数据权限注解 + * @date 2020-11-26 + */ +import java.lang.annotation.*; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataPermission { + + /** + * 只在类的注解上使用,代表方法的数据权限类型 + * @return + */ + String permission() default ""; + + /** + * 不需要数据权限的方法名 + * @return + */ + String[] ignoresMethod() default {}; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/EnumValue.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/EnumValue.java new file mode 100644 index 0000000..33804bf --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/EnumValue.java @@ -0,0 +1,88 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.annotation; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * @description 接口枚举类检测标注类 + * @date 2020-05-21 + */ +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = EnumValue.Validator.class) +public @interface EnumValue { + + String message() default "custom.value.invalid"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + Class> enumClass(); + + String enumMethod(); + + class Validator implements ConstraintValidator { + + private Class> enumClass; + private String enumMethod; + + @Override + public void initialize(EnumValue enumValue) { + enumMethod = enumValue.enumMethod(); + enumClass = enumValue.enumClass(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { + if (value == null) { + return Boolean.TRUE; + } + + if (enumClass == null || enumMethod == null) { + return Boolean.TRUE; + } + Class valueClass = value.getClass(); + + try { + Method method = enumClass.getMethod(enumMethod, valueClass); + if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) { + throw new RuntimeException(String.format("%s method return is not boolean type in the %s class", enumMethod, enumClass)); + } + + Boolean result = (Boolean)method.invoke(null, value); + return result == null ? false : result; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(String.format("This %s(%s) method does not exist in the %s", enumMethod, valueClass, enumClass), e); + } + } + + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/FlagValidator.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/FlagValidator.java new file mode 100644 index 0000000..4195cdb --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/FlagValidator.java @@ -0,0 +1,66 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.annotation; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Payload; +import java.lang.annotation.*; +import java.util.Arrays; + +/** + * @description 自定义状态校验注解(传入值是否在指定状态范围内) + * @date 2020-09-18 + */ +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = FlagValidator.Validator.class) +@Documented +public @interface FlagValidator { + + String[] value() default {}; + + String message() default "flag value is invalid"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * @description 校验传入值是否在默认值范围校验逻辑 + * @date 2020-09-18 + */ + class Validator implements ConstraintValidator { + + private String[] values; + + @Override + public void initialize(FlagValidator flagValidator) { + this.values = flagValidator.value(); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) { + if (value == null) { + //当状态为空时,使用默认值 + return false; + } + return Arrays.stream(values).anyMatch(Integer.toString(value)::equals); + } + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/Log.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/Log.java new file mode 100644 index 0000000..49ffc9b --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/annotation/Log.java @@ -0,0 +1,23 @@ + /** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.dubhe.biz.base.annotation; + +/** + * @description 日志 + * @date 2020-03-15 + */ +public @interface Log { +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ApplicationNameConst.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ApplicationNameConst.java new file mode 100644 index 0000000..34d61d8 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ApplicationNameConst.java @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.constant; + +/** + * @description 服务名常量类 spring.application.name + * @date 2020-11-05 + */ +public class ApplicationNameConst { + private ApplicationNameConst() { + + } + + /** + * 授权中心 + */ + public final static String SERVER_AUTHORIZATION = "auth"; + /** + * 网关 + */ + public final static String SERVER_GATEWAY = "gateway"; + /** + * 模型开发 + */ + public final static String SERVER_NOTEBOOK = "dubhe-notebook"; + /** + * 算法管理 + */ + public final static String SERVER_ALGORITHM = "dubhe-algorithm"; + /** + * 模型管理 + */ + public final static String SERVER_MODEL = "dubhe-model"; + /** + * 系统管理 + */ + public final static String SERVER_ADMIN = "admin"; + /** + * 镜像管理 + */ + public final static String SERVER_IMAGE = "dubhe-image"; + /** + * 度量管理 + */ + public final static String SERVER_MEASURE = "dubhe-measure"; + /** + * 训练管理 + */ + public final static String SERVER_TRAIN = "dubhe-train"; + /** + * 模型优化 + */ + public final static String SERVER_OPTIMIZE = "dubhe-optimize"; + /** + * 数据集管理 + */ + public final static String SERVER_DATA = "dubhe-data"; + + /** + * 云端Serving + */ + public final static String SERVER_SERVING = "dubhe-serving"; + + + /** + * 医学服务 + */ + public final static String SERVER_DATA_DCM = "dubhe-data-dcm"; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/AuthConst.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/AuthConst.java new file mode 100644 index 0000000..384679a --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/AuthConst.java @@ -0,0 +1,77 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.constant; + +/** + * @description 授权常量类 + * @date 2020-11-05 + */ +public class AuthConst { + private AuthConst() { + + } + + /** + * 授权token名称 Header + */ + public final static String AUTHORIZATION = "Authorization"; + /** + * 授权token名称 Params + */ + public final static String ACCESS_TOKEN = "access_token"; + /** + * token前缀 + */ + public final static String ACCESS_TOKEN_PREFIX = "Bearer "; + /** + * 客户端安全码 + * $2a$10$RUYBRsyV2jpG7pvg/VNus.YHVebzfRen3RGeDe1LVEIJeHYe2F1YK + */ + public final static String CLIENT_SECRET = "dubhe-secret"; + /** + * 客户端安全码 + */ + public final static String CLIENT_ID = "dubhe-client"; + /** + * 授权中心token校验地址 + */ + public final static String CHECK_TOKEN_ENDPOINT_URL = "http://" + ApplicationNameConst.SERVER_AUTHORIZATION + "/oauth/check_token"; + /** + * 默认匿名访问路径 + */ + public final static String[] DEFAULT_PERMIT_PATHS = {"/swagger**/**", "/webjars/**", "/v2/api-docs/**", "/doc.html/**", + "/users/findUserByUsername", "/auth/login", "/auth/code", + "/datasets/files/annotations/auto","/datasets/versions/**/convert/finish", "/datasets/enhance/finish", + "/auth/getCodeBySentEmail","/auth/userRegister", + StringConstant.RECYCLE_CALL_URI+"**" + }; + + /** + * k8s 回调token key + */ + public static final String K8S_CALLBACK_TOKEN = "k8sCallbackToken"; + + /** + * 通用授权token key + */ + public static final String COMMON_TOKEN = "commonToken"; + + /** + * 授权模式 + */ + public static final String GRANT_TYPE = "password"; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/DataStateCodeConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/DataStateCodeConstant.java new file mode 100644 index 0000000..cae8bc3 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/DataStateCodeConstant.java @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.constant; + +/** + * @description 数据集状态码 + * @date 2020-09-03 + */ +public class DataStateCodeConstant { + + private DataStateCodeConstant() { + } + + /** + * 未标注 + */ + public static final Integer NOT_ANNOTATION_STATE = 101; + /** + * 手动标注中 + */ + public static final Integer MANUAL_ANNOTATION_STATE = 102; + /** + * 自动标注中 + */ + public static final Integer AUTOMATIC_LABELING_STATE = 103; + /** + * 自动标注完成 + */ + public static final Integer AUTO_TAG_COMPLETE_STATE = 104; + /** + * 标注完成 + */ + public static final Integer ANNOTATION_COMPLETE_STATE = 105; + + /** + * 目标跟踪中 + */ + public static final Integer TARGET_FOLLOW_STATE = 201; + /** + * 目标跟踪完成 + */ + public static final Integer TARGET_COMPLETE_STATE = 202; + /** + * 目标跟踪失败 + */ + public static final Integer TARGET_FAILURE_STATE = 203; + + /** + * 未采样 + */ + public static final Integer NOT_SAMPLED_STATE = 301; + /** + * 采样中 + */ + public static final Integer SAMPLING_STATE = 302; + /** + * 采样失败 + */ + public static final Integer SAMPLED_FAILURE_STATE = 303; + + /** + * 增强中 + */ + public static final Integer STRENGTHENING_STATE = 401; + /** + * 导入中 + */ + public static final Integer IN_THE_IMPORT_STATE = 402; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/FileStateCodeConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/FileStateCodeConstant.java new file mode 100644 index 0000000..3eb54c0 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/FileStateCodeConstant.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.constant; + +/** + * @description 数据集状态码 + * @date 2020-09-03 + */ +public class FileStateCodeConstant { + + private FileStateCodeConstant(){ + + } + + /** + * 未标注 + */ + public static final Integer NOT_ANNOTATION_FILE_STATE = 101; + /** + * 手动标注中 + */ + public static final Integer MANUAL_ANNOTATION_FILE_STATE = 102; + /** + * 自动标注完成 + */ + public static final Integer AUTO_TAG_COMPLETE_FILE_STATE = 103; + /** + * 标注完成 + */ + public static final Integer ANNOTATION_COMPLETE_FILE_STATE = 104; + /** + * 标注未识别 + */ + public static final Integer ANNOTATION_NOT_DISTINGUISH_FILE_STATE = 105; + /** + * 目标跟踪完成 + */ + public static final Integer TARGET_COMPLETE_FILE_STATE = 201; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/HarborProperties.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/HarborProperties.java new file mode 100644 index 0000000..ed6ba31 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/HarborProperties.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.constant; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @description harbor相关配置 + * @date 2020-07-17 + */ +@Data +@Component +@ConfigurationProperties(prefix = "harbor") +public class HarborProperties { + + private String address; + + private String username; + + private String password; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MagicNumConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MagicNumConstant.java new file mode 100644 index 0000000..fe03ad6 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MagicNumConstant.java @@ -0,0 +1,109 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + +/** + * @description 常用常量类 + * @date 2020-05-14 + */ +public final class MagicNumConstant { + + public static final int NEGATIVE_ONE = -1; + public static final int ZERO = 0; + public static final int ONE = 1; + public static final int TWO = 2; + public static final int THREE = 3; + public static final int FOUR = 4; + public static final int FIVE = 5; + public static final int SIX = 6; + public static final int SEVEN = 7; + public static final int EIGHT = 8; + public static final int NINE = 9; + public static final int TEN = 10; + + public static final int ELEVEN = 11; + public static final int SIXTEEN = 16; + public static final int TWENTY = 20; + public static final int THIRTY_TWO = 32; + public static final int FIFTY = 50; + public static final int SIXTY = 60; + public static final int SIXTY_TWO = 62; + public static final int SIXTY_FOUR = 64; + public static final int ONE_HUNDRED = 100; + public static final int ONE_HUNDRED_TWENTY_SEVEN = 127; + public static final int ONE_HUNDRED_TWENTY_EIGHT = 128; + public static final int ONE_HUNDRED_SIXTY_EIGHT = 168; + public static final int TWO_HUNDRED = 200; + public static final int INTEGER_TWO_HUNDRED_AND_FIFTY_FIVE = 255; + public static final int FIVE_HUNDRED = 500; + public static final int FIVE_HUNDRED_AND_SIXTEEN = 516; + public static final int ONE_THOUSAND = 1000; + public static final int BINARY_TEN_EXP = 1024; + public static final int ONE_THOUSAND_ONE_HUNDRED = 1100; + public static final int ONE_THOUSAND_ONE_HUNDRED_ONE = 1101; + public static final int ONE_THOUSAND_TWO_HUNDRED = 1200; + public static final int ONE_THOUSAND_TWO_HUNDRED_ONE = 1201; + public static final int ONE_THOUSAND_TWENTY_FOUR = 1024; + public static final int ONE_THOUSAND_THREE_HUNDRED = 1300; + public static final int ONE_THOUSAND_THREE_HUNDRED_ONE = 1301; + public static final int ONE_THOUSAND_THREE_HUNDRED_NINE = 1309; + public static final int ONE_THOUSAND_FIVE_HUNDRED = 1500; + public static final int TWO_THOUSAND = 2000; + public static final int TWO_THOUSAND_TWENTY_EIGHT = 2048; + public static final int THREE_THOUSAND = 3000; + public static final int FOUR_THOUSAND = 4000; + public static final int NINE_THOUSAND = 9000; + public static final int NINE_THOUSAND_NINE_HUNDRED_NINTY_NINE = 9999; + public static final int TEN_THOUSAND = 10000; + public static final int FIFTEEN_THOUSAND = 15000; + public static final int HUNDRED_THOUSAND = 100000; + public static final int MILLION = 1000000; + public static final int ONE_MINUTE = 60000; + public static final int TWO_BILLION = 2000000000; + + public static final long NEGATIVE_ONE__LONG = -1L; + public static final long ZERO_LONG = 0L; + public static final long ONE_LONG = 1L; + public static final long TWO_LONG = 2L; + public static final long THREE_LONG = 3L; + public static final long FOUR_LONG = 4L; + public static final long FIVE_LONG = 5L; + public static final long SIX_LONG = 6L; + public static final long SEVEN_LONG = 7L; + public static final long EIGHT_LONG = 8L; + public static final long NINE_LONG = 9L; + public static final long TEN_LONG = 10L; + + public static final long TWELVE_LONG = 12L; + public static final long SIXTY_LONG = 60L; + public static final long THOUSAND_LONG = 1000L; + public static final long TEN_THOUSAND_LONG = 10000L; + public static final long MILLION_LONG = 1000000L; + public static final long ONE_ZERO_ONE_ZERO_ONE_ZERO_LONG = 101010L; + public static final long NINE_ZERO_NINE_ZERO_NINE_ZERO_LONG = 909090L; + public static final long ONE_YEAR_BEFORE_LONG = 1552579200000L; + + public static final double ZERO_DOUBLE = 0.0; + public static final double ONE_DOUBLE = 1.0; + + public static final int SIXITY_0XFF = 0xFF; + + + private MagicNumConstant() { + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MetaHandlerConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MetaHandlerConstant.java new file mode 100644 index 0000000..338bd17 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/MetaHandlerConstant.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + +/** + * @description 元数据枚举 + * @date 2020-11-26 + */ +public final class MetaHandlerConstant { + + /** + * 创建时间字段 + */ + public static final String CREATE_TIME = "createTime"; + /** + *更新时间字段 + */ + public static final String UPDATE_TIME = "updateTime"; + /** + *更新人id字段 + */ + public static final String UPDATE_USER_ID = "updateUserId"; + /** + *创建人id字段 + */ + public static final String CREATE_USER_ID = "createUserId"; + /** + *资源拥有人id字段 + */ + public static final String ORIGIN_USER_ID = "originUserId"; + /** + *删除字段 + */ + public static final String DELETED = "deleted"; + + private MetaHandlerConstant() { + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/NumberConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/NumberConstant.java new file mode 100644 index 0000000..bb177ef --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/NumberConstant.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + +/** + * @description 数字常量 + * @date 2020-06-09 + */ +public class NumberConstant { + + public final static int NUMBER_0 = 0; + public final static int NUMBER_1 = 1; + public final static int NUMBER_2 = 2; + public final static int NUMBER_3 = 3; + public final static int NUMBER_4 = 4; + public final static int NUMBER_5 = 5; + public final static int NUMBER_6 = 6; + public final static int NUMBER_8 = 8; + public final static int NUMBER_10 = 10; + public final static int NUMBER_30 = 30; + public final static int NUMBER_32 = 32; + public final static int NUMBER_50 = 50; + public final static int NUMBER_60 = 60; + public final static int NUMBER_64 = 64; + public final static int NUMBER_100 = 100; + public final static int NUMBER_1024 = 1024; + public final static int NUMBER_1000 = 1000; + public final static int HOUR_SECOND = 60 * 60; + public final static int DAY_SECOND = 60 * 60 * 24; + public final static int WEEK_SECOND = 60 * 60 * 24 * 7; + public final static int MAX_PAGE_SIZE = 2000; + public final static int MAX_MESSAGE_LENGTH = 1024 * 1024 * 1024; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/Permissions.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/Permissions.java new file mode 100644 index 0000000..5946fad --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/Permissions.java @@ -0,0 +1,205 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + +/** + * @description 权限标识,对应 menu 表中的 permission 字段 + * @date 2020-05-14 + */ +public final class Permissions { + + /** + * 数据管理 + */ + public static final String DATA = "hasAuthority('ROLE_data')"; + + /** + * notebook权限 + */ + public static final String NOTEBOOK = "hasAuthority('ROLE_notebook')"; + public static final String NOTEBOOK_CREATE = "hasAuthority('ROLE_notebook:create')"; + public static final String NOTEBOOK_UPDATE = "hasAuthority('ROLE_notebook:update')"; + public static final String NOTEBOOK_OPEN = "hasAuthority('ROLE_notebook:open')"; + public static final String NOTEBOOK_START = "hasAuthority('ROLE_notebook:start')"; + public static final String NOTEBOOK_STOP = "hasAuthority('ROLE_notebook:stop')"; + public static final String NOTEBOOK_DELETE = "hasAuthority('ROLE_notebook:delete')"; + + /** + * 算法管理 + */ + public static final String DEVELOPMENT_ALGORITHM = "hasAuthority('ROLE_development:algorithm')"; + public static final String DEVELOPMENT_ALGORITHM_CREATE = "hasAuthority('ROLE_development:algorithm:create')"; + public static final String DEVELOPMENT_ALGORITHM_EDIT = "hasAuthority('ROLE_development:algorithm:edit')"; + public static final String DEVELOPMENT_ALGORITHM_DELETE = "hasAuthority('ROLE_development:algorithm:delete')"; + + /** + * 训练管理 + */ + public static final String TRAINING_JOB = "hasAuthority('ROLE_training:job')"; + public static final String TRAINING_JOB_CREATE = "hasAuthority('ROLE_training:job:create')"; + public static final String TRAINING_JOB_UPDATE = "hasAuthority('ROLE_training:job:update')"; + public static final String TRAINING_JOB_DELETE = "hasAuthority('ROLE_training:job:delete')"; + + + public static final String MODEL_MODEL = "hasAuthority('ROLE_model:model')"; + public static final String MODEL_MODEL_CREATE = "hasAuthority('ROLE_model:model:create')"; + public static final String MODEL_MODEL_EDIT = "hasAuthority('ROLE_model:model:edit')"; + public static final String MODEL_MODEL_DELETE = "hasAuthority('ROLE_model:model:delete')"; + + /** + * 模型版本管理 + */ + public static final String MODEL_BRANCH = "hasAuthority('ROLE_model:branch')"; + public static final String MODEL_BRANCH_CREATE = "hasAuthority('ROLE_model:branch:create')"; + public static final String MODEL_BRANCH_DELETE = "hasAuthority('ROLE_model:branch:delete')"; + public static final String MODEL_BRANCH_CONVERT_PRESET = "hasAuthority('ROLE_model:branch:convertPreset')"; + public static final String MODEL_BRANCH_CONVERT_ONNX = "hasAuthority('ROLE_model:branch:convertOnnx')"; + + /** + * 模型优化 + */ + public static final String MODEL_OPTIMIZE = "hasAuthority('ROLE_model:optimize')"; + public static final String MODEL_OPTIMIZE_CREATE = "hasAuthority('ROLE_model:optimize:createTask')"; + public static final String MODEL_OPTIMIZE_SUBMIT_TASK = "hasAuthority('ROLE_model:optimize:submitTask')"; + public static final String MODEL_OPTIMIZE_SUBMIT_TASK_INSTANCE = "hasAuthority('ROLE_model:optimize:submitTaskInstance')"; + public static final String MODEL_OPTIMIZE_CANCEL_TASK_INSTANCE = "hasAuthority('ROLE_model:optimize:cancelTaskInstance')"; + public static final String MODEL_OPTIMIZE_EDIT = "hasAuthority('ROLE_model:optimize:editTask')"; + public static final String MODEL_OPTIMIZE_DELETE_TASK = "hasAuthority('ROLE_model:optimize:deleteTask')"; + public static final String MODEL_OPTIMIZE_DELETE_TASK_INSTANCE = "hasAuthority('ROLE_model:optimize:deleteTaskInstance')"; + + /** + * 控制台 + */ + public static final String SYSTEM_NODE = "hasAuthority('ROLE_system:node')"; + public static final String SYSTEM_LOG = "hasAuthority('ROLE_system:log')"; + public static final String SYSTEM_TEAM = "hasAuthority('ROLE_system:team')"; + + /** + * 云端Serving + */ + public static final String SERVING = "hasAuthority('ROLE_serving')"; + public static final String SERVING_BATCH = "hasAuthority('ROLE_serving:batch')"; + public static final String SERVING_BATCH_CREATE = "hasAuthority('ROLE_serving:batch:create')"; + public static final String SERVING_BATCH_EDIT = "hasAuthority('ROLE_serving:batch:edit')"; + public static final String SERVING_BATCH_START = "hasAuthority('ROLE_serving:batch:start')"; + public static final String SERVING_BATCH_STOP = "hasAuthority('ROLE_serving:batch:stop')"; + public static final String SERVING_BATCH_DELETE = "hasAuthority('ROLE_serving:batch:delete')"; + public static final String SERVING_DEPLOYMENT = "hasAuthority('ROLE_serving:online')"; + public static final String SERVING_DEPLOYMENT_CREATE = "hasAuthority('ROLE_serving:online:create')"; + public static final String SERVING_DEPLOYMENT_EDIT = "hasAuthority('ROLE_serving:online:edit')"; + public static final String SERVING_DEPLOYMENT_DELETE = "hasAuthority('ROLE_serving:online:delete')"; + public static final String SERVING_DEPLOYMENT_START = "hasAuthority('ROLE_serving:online:start')"; + public static final String SERVING_DEPLOYMENT_STOP = "hasAuthority('ROLE_serving:online:stop')"; + + /** + * 镜像管理 + */ + public static final String IMAGE = "hasAuthority('ROLE_training:image')"; + public static final String IMAGE_UPLOAD = "hasAuthority('ROLE_training:image:upload')"; + public static final String IMAGE_EDIT = "hasAuthority('ROLE_training:image:edit')"; + public static final String IMAGE_DELETE = "hasAuthority('ROLE_training:image:delete')"; + + /** + * 度量管理 + */ + public static final String MEASURE = "hasAuthority('ROLE_atlas:measure')"; + public static final String MEASURE_CREATE = "hasAuthority('ROLE_atlas:measure:create')"; + public static final String MEASURE_EDIT = "hasAuthority('ROLE_atlas:measure:edit')"; + public static final String MEASURE_DELETE = "hasAuthority('ROLE_atlas:measure:delete')"; + + /** + * 控制台:用户组管理 + */ + public static final String USER_GROUP_CREATE = "hasAuthority('ROLE_system:userGroup:create')"; + public static final String USER_GROUP_EDIT = "hasAuthority('ROLE_system:userGroup:edit')"; + public static final String USER_GROUP_DELETE = "hasAuthority('ROLE_system:userGroup:delete')"; + public static final String USER_GROUP_EDIT_USER = "hasAuthority('ROLE_system:userGroup:editUser')"; + public static final String USER_GROUP_EDIT_USER_ROLE = "hasAuthority('ROLE_system:userGroup:editUserRole')"; + public static final String USER_GROUP_EDIT_USER_STATE = "hasAuthority('ROLE_system:userGroup:editUserState')"; + public static final String USER_GROUP_DELETE_USER = "hasAuthority('ROLE_system:userGroup:deleteUser')"; + + /** + * 控制台:用户管理 + */ + public static final String USER_CREATE = "hasAuthority('ROLE_system:user:create')"; + public static final String USER_EDIT = "hasAuthority('ROLE_system:user:edit')"; + public static final String USER_DELETE = "hasAuthority('ROLE_system:user:delete')"; + public static final String USER_DOWNLOAD = "hasAuthority('ROLE_system:user:download')"; + + /** + * 控制台:角色管理 + */ + public static final String ROLE = "hasAuthority('ROLE_system:role')"; + public static final String ROLE_CREATE = "hasAuthority('ROLE_system:role:create')"; + public static final String ROLE_EDIT = "hasAuthority('ROLE_system:role:edit')"; + public static final String ROLE_DELETE = "hasAuthority('ROLE_system:role:delete')"; + public static final String ROLE_DWONLOAD = "hasAuthority('ROLE_system:role:download')"; + public static final String ROLE_MENU = "hasAuthority('ROLE_system:role:menu')"; + public static final String ROLE_AUTH = "hasAuthority('ROLE_system:role:auth')"; + + /** + * 控制台:权限组管理 + */ + public static final String AUTH_CODE = "hasAuthority('ROLE_system:authCode')"; + public static final String AUTH_CODE_CREATE = "hasAuthority('ROLE_system:authCode:create')"; + public static final String AUTH_CODE_EDIT = "hasAuthority('ROLE_system:authCode:edit')"; + public static final String AUTH_CODE_DELETE = "hasAuthority('ROLE_system:authCode:delete')"; + + /** + * 控制台:权限管理 + */ + public static final String PERMISSION = "hasAuthority('ROLE_system:permission')"; + public static final String PERMISSION_CREATE = "hasAuthority('ROLE_system:permission:create')"; + public static final String PERMISSION_EDIT = "hasAuthority('ROLE_system:permission:edit')"; + public static final String PERMISSION_DELETE = "hasAuthority('ROLE_system:permission:delete')"; + + /** + * 控制台:菜单管理 + */ + public static final String MENU = "hasAuthority('ROLE_system:menu')"; + public static final String MENU_CREATE = "hasAuthority('ROLE_system:menu:create')"; + public static final String MENU_EDIT = "hasAuthority('ROLE_system:menu:edit')"; + public static final String MENU_DELETE = "hasAuthority('ROLE_system:menu:delete')"; + public static final String MENU_DOWNLOAD = "hasAuthority('ROLE_system:menu:download')"; + + /** + * 控制台:字典管理 + */ + public static final String DICT = "hasAuthority('ROLE_system:dict')"; + public static final String DICT_CREATE = "hasAuthority('ROLE_system:dict:create')"; + public static final String DICT_EDIT = "hasAuthority('ROLE_system:dict:edit')"; + public static final String DICT_DELETE = "hasAuthority('ROLE_system:dict:delete')"; + public static final String DICT_DOWNLOAD = "hasAuthority('ROLE_system:dict:download')"; + + /** + * 控制台:字典详情管理 + */ + public static final String DICT_DETAIL_CREATE = "hasAuthority('ROLE_system:dictDetail:create')"; + public static final String DICT_DETAIL_EDIT = "hasAuthority('ROLE_system:dictDetail:edit')"; + public static final String DICT_DETAIL_DELETE = "hasAuthority('ROLE_system:dictDetail:delete')"; + + /** + * 控制台:资源规格管理 + */ + public static final String SPECS_CREATE = "hasAuthority('ROLE_system:specs:create')"; + public static final String SPECS_EDIT = "hasAuthority('ROLE_system:specs:edit')"; + public static final String SPECS_DELETE = "hasAuthority('ROLE_system:specs:delete')"; + + private Permissions() { + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ResponseCode.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ResponseCode.java new file mode 100644 index 0000000..2c52142 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/ResponseCode.java @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + +/** + * @description 业务返回状态码 + * @date 2020-02-23 + */ +public class ResponseCode { + public static Integer SUCCESS = 200; + public static Integer UNAUTHORIZED = 401; + public static Integer TOKEN_ERROR = 403; + public static Integer ERROR = 10000; + public static Integer ENTITY_NOT_EXIST = 10001; + public static Integer BADREQUEST = 10002; + public static Integer SERVICE_ERROR = 10003; + public static Integer DOCKER_ERROR = 10004; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/StringConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/StringConstant.java new file mode 100644 index 0000000..fd10aee --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/StringConstant.java @@ -0,0 +1,115 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + +import java.util.regex.Pattern; + +/** + * @description 字符串constant + * @date 2020-05-14 + */ +public final class StringConstant { + + public static final String MSIE = "MSIE"; + public static final String MOZILLA = "Mozilla"; + public static final String REQUEST_METHOD_GET = "GET"; + + /** + * 字母、数字、英文横杠和下划线匹配 + */ + public static final String REGEXP_NAME = "^[a-zA-Z0-9\\-\\_\\u4e00-\\u9fa5]+$"; + + /** + * 字母、数字、英文横杠、英文.号和下划线 + */ + public static final String REGEXP_TAG = "^[a-zA-Z0-9\\-\\_\\.]+$"; + + /** + * 算法名称支持字母、数字、汉字、英文横杠和下划线 + */ + public static final String REGEXP_ALGORITHM = "^[a-zA-Z0-9\\-\\_\\u4e00-\\u9fa5]+$"; + + /** + * 资源规格名称支持字母、数字、汉字、英文横杠、下划线和空白字符 + */ + public static final String REGEXP_SPECS = "^[a-zA-Z0-9\\-\\_\\s\\u4e00-\\u9fa5]+$"; + + /** + * 整数匹配 + */ + public static final Pattern PATTERN_NUM = Pattern.compile("^[-\\+]?[\\d]*$"); + + + /** + * 公共字段 + */ + public static final String CREATE_TIME = "createTime"; + public static final String UPDATE_TIME = "updateTime"; + public static final String UPDATE_USER_ID = "updateUserId"; + public static final String CREATE_USER_ID = "createUserId"; + public static final String ORIGIN_USER_ID = "originUserId"; + public static final String DELETED = "deleted"; + public static final String UTF8 = "utf-8"; + public static final String JSON_REQUEST = "application/json"; + public static final String CREATE_TIME_SQL = "create_time"; + public static final String UPDATE_TIME_SQL = "update_time"; + public static final String COMMON = "common"; + /** + * k8s回调公共路径 + */ + public static final String K8S_CALLBACK_URI = "/api/k8s/callback/pod"; + /** + * 资源回收远程调用路径 + */ + public static final String RECYCLE_CALL_URI = "/api/recycle/call/"; + public static final String K8S_CALLBACK_PATH_DEPLOYMENT = "/api/k8s/callback/deployment"; + public static final String MULTIPART = "multipart/form-data"; + /** + * 分页内容 + */ + public static final String RESULT = "result"; + /** + * 排序规则 + */ + public static final String SORT_ASC = "asc"; + + public static final String SORT_DESC = "desc"; + + public static final String QUERY = "query"; + + public static final String NGINX_LOWERCASE = "nginx"; + + public static final String TRUE_LOWERCASE = "true"; + + public static final String GRPC_CAPITALIZE = "GRPC"; + + public static final String ID = "id"; + + public static final String START_LOW = "start"; + public static final String END_LOW = "end"; + public static final String STEP_LOW = "step"; + + /** + * 测试环境 + */ + public static final String PROFILE_ACTIVE_TEST = "test"; + + + private StringConstant() { + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/SymbolConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/SymbolConstant.java new file mode 100644 index 0000000..4190208 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/SymbolConstant.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + +/** + * @description 符号常量 + * @date 2020-5-29 + */ +public class SymbolConstant { + public static final String SLASH = "/"; + public static final String COMMA = ","; + public static final String COLON = ":"; + public static final String LINEBREAK = "\n"; + public static final String BLANK = ""; + public static final String QUESTION = "?"; + public static final String ZERO = "0"; + public static final String DOT = "."; + public static final String TOKEN = "token"; + public static final String GET = "get"; + public static final String SET = "set"; + public static final String HTTP = "http"; + public static final String GRPC = "grpc"; + public static final String BRACKETS = "{}"; + public static final String BACKSLASH = "\\"; + public static final String BACKSLASH_MARK= "\\\""; + public static final String DOUBLE_MARK= "\"\""; + public static final String MARK= "\""; + public static final String FLAG_EQUAL = "="; + public static final String LEFT_PARENTHESIS = "["; + public static final String RIGHT_PARENTHESIS = "]"; + public static final String APOSTROPHE = "'"; + public static final String HYPHEN = "-"; + public static final String EVENT_SEPARATOR = "&&"; + public static final String POST = "POST"; + public static final String HTTP_SLASH = "http://"; + + private SymbolConstant() { + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/UserConstant.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/UserConstant.java new file mode 100644 index 0000000..c382617 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/constant/UserConstant.java @@ -0,0 +1,94 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.constant; + + +/** + * @description 用户常量类 + * @date 2020-06-01 + */ +public class UserConstant { + + public final static String SEX_MEN = "男"; + + public final static String SEX_WOMEN = "女"; + + /** + * redis key + */ + /** + * 用户发送邮件限制次数 + */ + public final static String USER_EMAIL_LIMIT_COUNT = "user:email:limit:count:"; + + /** + * 用户邮箱注册信息发送验证码 + */ + public final static String USER_EMAIL_REGISTER = "user:email:register:"; + + /** + * 用户邮箱激修改信息发送验证码 + */ + public final static String USER_EMAIL_UPDATE = "user:email:update:"; + + /** + * 用户邮箱忘记发送验证码 + */ + public final static String USER_EMAIL_RESET_PASSWORD = "user:email:reset-password:"; + + /** + * 用户其他操作发送验证码 + */ + public final static String USER_EMAIL_OTHER = "user:email:other:"; + + /** + * 一天的秒数 24x60x60 + */ + public final static long DATE_SECOND = 86400; + + /** + * 发邮件次数 + */ + public final static int COUNT_SENT_EMAIL = 3; + + /** + * 账号密码不正确登录失败次数 + */ + public final static int COUNT_LOGIN_FAIL = 5; + + /** + * 用户登录限制次数 + */ + public final static String USER_LOGIN_LIMIT_COUNT = "user:login:limit:count:"; + + /** + * 初始化管理员ID + */ + public final static Integer ADMIN_USER_ID = 1; + + /** + * 管理员角色ID + */ + public final static int ADMIN_ROLE_ID = 1; + + /** + * 注册用户角色ID + */ + public final static int REGISTER_ROLE_ID = 2; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/DataContext.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/DataContext.java new file mode 100644 index 0000000..b831c3b --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/DataContext.java @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.context; + + +import org.dubhe.biz.base.dto.CommonPermissionDataDTO; + +/** + * @description 共享上下文数据集信息 + * @date 2020-11-25 + */ +public class DataContext { + + /** + * 私有化构造参数 + */ + private DataContext() { + } + + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + + /** + * 存放数据集信息 + * + * @param datasetVO + */ + public static void set(CommonPermissionDataDTO datasetVO) { + CONTEXT.set(datasetVO); + } + + /** + * 获取用户信息 + * + * @return + */ + public static CommonPermissionDataDTO get() { + return CONTEXT.get(); + } + + /** + * 清除当前线程内引用,防止内存泄漏 + */ + public static void remove() { + CONTEXT.remove(); + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/UserContext.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/UserContext.java new file mode 100644 index 0000000..4a764e5 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/context/UserContext.java @@ -0,0 +1,76 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.context; + +import lombok.Data; +import org.dubhe.biz.base.dto.SysRoleDTO; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 用户上下文(当前登录用户) + * 可根据需要自定义改造 + * @date 2020-12-07 + */ +@Data +public class UserContext implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long id; + /** + * 用户名称 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 邮箱 + */ + private String email; + /** + * 性别 + */ + private String sex; + /** + * 手机号 + */ + private String phone; + /** + * 昵称 + */ + private String nickName; + /** + * 是否启用 + */ + private Boolean enabled; + /** + * 角色 + */ + private List roles; + /** + * 头像路径 + */ + private String userAvatarPath; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseDTO.java new file mode 100644 index 0000000..d685032 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description DTO基础类 + * @date 2020-03-15 + */ +@Getter +@Setter +public class BaseDTO implements Serializable { + + private Boolean deleted; + + private Timestamp createTime; + + private Timestamp updateTime; + + @Override + public String toString() { + return "BaseDTO{" + + "deleted=" + deleted + + ", createTime=" + createTime + + ", updateTime=" + updateTime + + '}'; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseImageDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseImageDTO.java new file mode 100644 index 0000000..975efa4 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/BaseImageDTO.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * @description 镜像基础类DTO + * @date 2020-07-14 + */ +@Data +@Accessors(chain = true) +public class BaseImageDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 镜像版本 + */ + @NotBlank(message = "镜像版本不能为空") + private String imageTag; + + /** + * 镜像名称 + */ + @NotBlank(message = "镜像名称不能为空") + private String imageName; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/CommonPermissionDataDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/CommonPermissionDataDTO.java new file mode 100644 index 0000000..0887463 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/CommonPermissionDataDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serializable; +import java.util.Set; + +/** + * @description 公共权限信息DTO + * @date 2020-11-25 + */ +@Data +@Builder +public class CommonPermissionDataDTO implements Serializable { + + /** + * 资源拥有者ID + */ + private Long id; + + /** + * 公共类型 + */ + private Boolean type; + + /** + * 资源所属用户ids + */ + private Set resourceUserIds; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/DictDetailQueryByLabelNameDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/DictDetailQueryByLabelNameDTO.java new file mode 100644 index 0000000..1511999 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/DictDetailQueryByLabelNameDTO.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * @description 根据名称查询字典详情 + * @date 2020-12-23 + */ +@Data +public class DictDetailQueryByLabelNameDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotBlank(message = "Label名称不能为空") + private String name; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/ModelOptAlgorithmCreateDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/ModelOptAlgorithmCreateDTO.java new file mode 100644 index 0000000..df7014c --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/ModelOptAlgorithmCreateDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.constant.StringConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 模型优化上传算法入参 + * @date 2021-01-06 + */ +@Data +@Accessors(chain = true) +public class ModelOptAlgorithmCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotBlank(message = "算法名称不能为空") + @Length(max = NumberConstant.NUMBER_32, message = "算法名称-输入长度不能超过32个字符") + @Pattern(regexp = StringConstant.REGEXP_ALGORITHM, message = "算法名称支持字母、数字、汉字、英文横杠和下划线") + private String name; + + @NotBlank(message = "代码目录不能为空") + @Length(max = NumberConstant.NUMBER_64, message = "代码目录-输入长度不能超过128个字符") + private String path; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmQueryDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmQueryDTO.java new file mode 100644 index 0000000..2fb230a --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmQueryDTO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.List; + +/** + * @description 算法查询notebook对象 + * @date 2020-12-14 + */ +@Data +public class NoteBookAlgorithmQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 算法ID + */ + @NotEmpty(message = "算法ID不能为空") + private List algorithmIdList; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmUpdateDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmUpdateDTO.java new file mode 100644 index 0000000..310f9db --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/NoteBookAlgorithmUpdateDTO.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * @description 算法更新notebook对象 + * @date 2020-12-14 + */ +@Data +public class NoteBookAlgorithmUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * NoteBookID + */ + @NotEmpty(message = "NoteBookID不能为空") + private List notebookIdList; + + /** + * 算法ID + */ + @NotNull(message = "算法ID不能为空") + private Long algorithmId; + + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/Oauth2TokenDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/Oauth2TokenDTO.java new file mode 100644 index 0000000..77a349a --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/Oauth2TokenDTO.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * @description Oauth2获取Token返回信息封装 + * @date 2020-05-08 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Builder +public class Oauth2TokenDTO implements Serializable { + /** + * 访问令牌 + */ + private String token; + /** + * 刷令牌 + */ + private String refreshToken; + /** + * 访问令牌头前缀 + */ + private String tokenHead; + /** + * 有效时间(秒 + */ + private int expiresIn; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtDatasetSmallDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtDatasetSmallDTO.java new file mode 100644 index 0000000..50fed5e --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtDatasetSmallDTO.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 数据集 + * @date 2020-03-17 + */ +@Data +public class PtDatasetSmallDTO implements Serializable { + private Long id; + private String name; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtImageQueryUrlDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtImageQueryUrlDTO.java new file mode 100644 index 0000000..9c7f7bc --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtImageQueryUrlDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @description 查询镜像路径 + * @date 2020-12-14 + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode +public class PtImageQueryUrlDTO { + + private Integer imageResource; + + private String imageName; + + private String imageTag; + + private Integer projectType; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchConditionQueryDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchConditionQueryDTO.java new file mode 100644 index 0000000..a12fd36 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchConditionQueryDTO.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 条件查询模型版本 + * @date 2020-03-24 + */ +@Data +public class PtModelBranchConditionQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull(message = "父ID不能为空") + private Long parentId; + + private String modelAddress; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchQueryByIdDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchQueryByIdDTO.java new file mode 100644 index 0000000..2f555bc --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelBranchQueryByIdDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 根据模型版本id查询模型版本详情入参 + * @date 2020-12-16 + */ +@Data +public class PtModelBranchQueryByIdDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模型版本ID + */ + @NotNull(message = "模型版本ID不能为空") + private Long id; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoConditionQueryDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoConditionQueryDTO.java new file mode 100644 index 0000000..836981c --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoConditionQueryDTO.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.utils.PtModelUtil; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 模型条件查询 + * @date 2020-12-17 + */ +@Data +@Accessors +public class PtModelInfoConditionQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模型是否为预置模型(0默认模型,1预置模型) + * + */ + @Min(value = PtModelUtil.NUMBER_ZERO, message = "模型类型错误") + @Max(value = PtModelUtil.NUMBER_TWO, message = "模型类型错误") + @NotNull(message = "modelResource不能为空") + private Integer modelResource; + + /** + * 模型id + */ + @NotNull(message = "id不能为空") + private Set ids; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoQueryByIdDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoQueryByIdDTO.java new file mode 100644 index 0000000..c05b5db --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelInfoQueryByIdDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 根据模型id查询模型详情入参 + * @date 2020-12-16 + */ +@Data +public class PtModelInfoQueryByIdDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模型ID + */ + @NotNull(message = "模型ID不能为空") + private Long id; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelStatusQueryDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelStatusQueryDTO.java new file mode 100644 index 0000000..a6af7b6 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtModelStatusQueryDTO.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 查询模型对应训练状态 + * @date 2021-03-04 + */ +@Data +@Accessors(chain = true) +public class PtModelStatusQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模型id + */ + private List modelIds; + + /** + * 模型对应版本id + */ + private List modelBranchIds; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtStorageSmallDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtStorageSmallDTO.java new file mode 100644 index 0000000..0e85478 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtStorageSmallDTO.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 存储 + * @date 2020-03-17 + */ +@Data +public class PtStorageSmallDTO implements Serializable { + private Long id; + private String name; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtTrainDataSourceStatusQueryDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtTrainDataSourceStatusQueryDTO.java new file mode 100644 index 0000000..5d07628 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/PtTrainDataSourceStatusQueryDTO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 查询数据集对应训练状态查询条件 + * @date 2020-05-21 + */ +@Data +@Accessors(chain = true) +public class PtTrainDataSourceStatusQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 数据集路径 + */ + private List dataSourcePath; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/QueryResourceSpecsDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/QueryResourceSpecsDTO.java new file mode 100644 index 0000000..08e48de --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/QueryResourceSpecsDTO.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 查询资源规格 + * @date 2021-06-02 + */ +@Data +@Accessors(chain = true) +public class QueryResourceSpecsDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 规格名称 + */ + @NotBlank(message = "规格名称不能为空") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "规格名称错误") + private String specsName; + + /** + * 所属业务场景(0:通用,1:dubhe-notebook,2:dubhe-train,3:dubhe-serving) + */ + @NotNull(message = "所属业务场景不能为空") + @Min(value = MagicNumConstant.ZERO, message = "所属业务场景错误") + @Max(value = MagicNumConstant.THREE, message = "所属业务场景错误") + private Integer module; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysPermissionDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysPermissionDTO.java new file mode 100644 index 0000000..c16c798 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysPermissionDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import java.io.Serializable; + + +/** + * @description 系统权限DTO + * @date 2020-06-29 + */ +@Data +public class SysPermissionDTO implements Serializable { + + private static final long serialVersionUID = -3836401769559845765L; + + private Long roleId; + + private String permission; + + private String name; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysRoleDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysRoleDTO.java new file mode 100644 index 0000000..33e6270 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/SysRoleDTO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + + +/** + * @description 系统角色DTO + * @date 2020-06-29 + */ +@Data +public class SysRoleDTO implements Serializable { + + private static final long serialVersionUID = -3836401769559845765L; + + /** + * 权限列表 + */ + private List permissions; + + /** + * 角色名称 + */ + private String name; + + /** + * 角色ID + */ + private Long id; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamDTO.java new file mode 100644 index 0000000..7dc48d8 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamDTO.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; +import org.dubhe.biz.base.dto.UserSmallDTO; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 团队转换DTO + * @date 2020-06-01 + */ +@Data +public class TeamDTO implements Serializable { + + private static final long serialVersionUID = -7049447715255649751L; + private Long id; + + private String name; + + private Boolean enabled; + + private Long pid; + + /** + * 团队成员 + */ + private List teamUserList; + + private Timestamp createTime; + + public String getLabel() { + return name; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamSmallDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamSmallDTO.java new file mode 100644 index 0000000..c916f15 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TeamSmallDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 团队DTO + * @date 2020-06-01 + */ +@Data +public class TeamSmallDTO implements Serializable { + + private static final long serialVersionUID = 7811877555052402740L; + + private Long id; + + private String name; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllBatchIdDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllBatchIdDTO.java new file mode 100644 index 0000000..4f4ea4a --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllBatchIdDTO.java @@ -0,0 +1,36 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 根据Id批量查询 + * @date 2020-12-23 + */ +@Data +public class TrainAlgorithmSelectAllBatchIdDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull(message = "id不能为空") + private Set ids; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllByIdDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllByIdDTO.java new file mode 100644 index 0000000..aa45476 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectAllByIdDTO.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 根据Id查询所有数据(包含已被软删除的数据) + * @date 2020-12-23 + */ +@Data +public class TrainAlgorithmSelectAllByIdDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull(message = "id不能为空") + private Long id; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectByIdDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectByIdDTO.java new file mode 100644 index 0000000..b831745 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/TrainAlgorithmSelectByIdDTO.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 根据Id查询 + * @date 2020-12-23 + */ +@Data +public class TrainAlgorithmSelectByIdDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull(message = "id不能为空") + private Long id; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserDTO.java new file mode 100644 index 0000000..1572a16 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserDTO.java @@ -0,0 +1,63 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.biz.base.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; + +/** + * @description 用户信息 + * @date 2020-06-29 + */ +@Data +public class UserDTO implements Serializable { + + private Long id; + + private String username; + + private String nickName; + + private String sex; + + private String email; + + private String phone; + + private Boolean enabled; + + private String remark; + + private Date lastPasswordResetTime; + + private Timestamp createTime; + + /** + * 头像路径 + */ + private String userAvatarPath; + + /** + * 角色 + */ + private List roles; + + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserSmallDTO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserSmallDTO.java new file mode 100644 index 0000000..d38065f --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/dto/UserSmallDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.dto; + +import lombok.Data; + +/** + * @description 用户信息DTO + * @date 2020-06-01 + */ +@Data +public class UserSmallDTO { + private String username; + private String nickName; + + public UserSmallDTO() {} + + public UserSmallDTO(UserDTO userDTO) { + this.username = userDTO.getUsername(); + this.nickName = userDTO.getNickName(); + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmSourceEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmSourceEnum.java new file mode 100644 index 0000000..4018dad --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmSourceEnum.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +import lombok.Getter; + +/** + * @description 算法枚举类 + * @date 2020-05-12 + */ +@Getter +public enum AlgorithmSourceEnum { + + /** + * MINE 算法来源 我的算法 + */ + MINE(1, "MINE"), + /** + * PRE 算法来源 预置算法 + */ + PRE(2,"PRE"); + + private Integer status; + + private String message; + + AlgorithmSourceEnum(Integer status, String message) { + this.status = status; + this.message = message; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmStatusEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmStatusEnum.java new file mode 100644 index 0000000..9087728 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/AlgorithmStatusEnum.java @@ -0,0 +1,63 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.enums; + +/** + * @description 算法状态枚举 + * @date 2020-08-19 + */ +public enum AlgorithmStatusEnum { + + + /** + * 创建中 + */ + MAKING(0, "创建中"), + /** + * 创建成功 + */ + SUCCESS(1, "创建成功"), + /** + * 创建失败 + */ + FAIL(2, "创建失败"); + + + /** + * 编码 + */ + private Integer code; + + /** + * 描述 + */ + private String description; + + AlgorithmStatusEnum(int code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } +} + diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BaseErrorCodeEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BaseErrorCodeEnum.java new file mode 100644 index 0000000..f942768 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BaseErrorCodeEnum.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +import lombok.Getter; +import org.dubhe.biz.base.exception.ErrorCode; + +/** + * @description 通用异常code + * @date 2020-11-26 + */ +@Getter +public enum BaseErrorCodeEnum implements ErrorCode { + + /** + * undefined error + */ + UNDEFINED(10000, "操作成功!"), + ERROR(10001, "操作失败!"), + ERROR_SYSTEM(10002, "系统繁忙!"), + UNAUTHORIZED(401, "无权访问!"), + /** + * system 模块异常码 + */ + SYSTEM_USERNAME_ALREADY_EXISTS(20000, "账号已存在!"), + SYSTEM_CODE_ALREADY_EXISTS(20001, "Code already exists!"), + SYSTEM_USER_IS_NOT_EXISTS(20002, "用户不存在!"), + SYSTEM_USER_ALREADY_REGISTER(20003, "账号已注册!"), + SYSTEM_USER_REGISTER_EMAIL_INFO_EXPIRED(20004, "邮箱验证码已过期!"), + SYSTEM_USER_EMAIL_ALREADY_EXISTS(20004, "该邮箱已被注册!"), + SYSTEM_USER_EMAIL_PASSWORD_ERROR(20005, "邮件密码错误!"), + SYSTEM_USER_EMAIL_CODE_CANNOT_EXCEED_TIMES(20006, "邮件发送不能超过三次!"), + SYSTEM_USER_EMAIL_OR_CODE_ERROR(20007, "邮箱地址或验证码错误、请重新输入!"), + SYSTEM_USER_IS_LOCKED(20008, "用户已锁定!"), + SYSTEM_USER_USERNAME_OR_PASSWORD_ERROR(20009, "账号或密码不正确!"), + SYSTEM_USER_USERNAME_IS_LOCKED(20010, "账号已锁定,6小时后解锁!"), + SYSTEM_USER_CANNOT_UPDATE_ADMIN(20011, "仅超级管理员可操作!"), + SYSTEM_USER_TOKEN_INFO_IS_NULL(20012, "登录信息不存在!"), + SYSTEM_USER_EMAIL_NOT_EXISTS(20013, "该邮箱未注册!"), + SYSTEM_USER_CANNOT_DELETE(20014, "系统默认用户不可删除!"), + SYSTEM_ROLE_CANNOT_DELETE(20015, "系统默认角色不可删除!"), + + DATASET_ADMIN_PERMISSION_ERROR(1310,"无此权限,请联系管理员"), + + ; + + + Integer code; + String msg; + + BaseErrorCodeEnum(Integer code, String msg) { + this.code = code; + this.msg = msg; + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BizEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BizEnum.java new file mode 100644 index 0000000..56579ff --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/BizEnum.java @@ -0,0 +1,98 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @description 业务模块 + * @date 2020-05-25 + */ +@Getter +public enum BizEnum { + + /** + * 模型开发 + */ + NOTEBOOK("模型开发", "notebook", 0), + /** + * 算法管理 + */ + ALGORITHM("算法管理", "algorithm", 1), + /** + * 模型管理 + */ + MODEL("模型管理", "model", 2), + /** + * 模型优化 + */ + MODEL_OPT("模型优化管理","modelopt",6), + /** + * 云端Serving-在线服务 + */ + SERVING("云端Serving", "serving", 3), + /** + * 批量服务 + */ + BATCH_SERVING("批量服务", "batchserving", 4), + /** + * 度量管理 + */ + MEASURE("度量管理", "measure", 5); + + /** + * 业务模块名称 + */ + private String bizName; + /** + * 业务模块名称 + */ + private String bizCode; + /** + * 业务源代号 + */ + private Integer createResource; + + BizEnum(String bizName, String bizCode, Integer createResource) { + this.createResource = createResource; + this.bizName = bizName; + this.bizCode = bizCode; + } + + private static final Map RESOURCE_ENUM_MAP = new HashMap() { + { + for (BizEnum enums : BizEnum.values()) { + put(enums.getCreateResource(), enums); + } + } + }; + + /** + * 根据createResource获取BizEnum + * @param createResource + * @return + */ + public static BizEnum getByCreateResource(int createResource) { + return RESOURCE_ENUM_MAP.get(createResource); + } + + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/DatasetTypeEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/DatasetTypeEnum.java new file mode 100644 index 0000000..8f6ca3d --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/DatasetTypeEnum.java @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + + +import lombok.Getter; + +/** + * @description 数据集类型 + * @date 2020-11-25 + */ +@Getter +public enum DatasetTypeEnum { + + /** + * 私有数据 + */ + PRIVATE(0, "私有数据"), + /** + * 团队数据 + */ + TEAM(1, "团队数据"), + /** + * 公开数据 + */ + PUBLIC(2, "公开数据"); + + DatasetTypeEnum(Integer value, String msg) { + this.value = value; + this.msg = msg; + } + + private Integer value; + + private String msg; + + /** + * 数据类型校验 用户web端接口调用时参数校验 + * + * @param value 数据类型 + * @return 参数校验结果 + */ + public static boolean isValid(Integer value) { + for (DatasetTypeEnum datasetTypeEnum : DatasetTypeEnum.values()) { + if (datasetTypeEnum.value.equals(value)) { + return true; + } + } + return false; + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageSourceEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageSourceEnum.java new file mode 100644 index 0000000..b8fbe6f --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageSourceEnum.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.enums; + +/** + * @description 镜像来源枚举类 + * @date 2020-07-15 + */ +public enum ImageSourceEnum { + MINE(0, "我的镜像"), + PRE(1, "预置镜像"); + + + /** + * 编码 + */ + private Integer code; + + /** + * 描述 + */ + private String description; + + ImageSourceEnum(int code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageStateEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageStateEnum.java new file mode 100644 index 0000000..27bdb8d --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageStateEnum.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +/** + * @description 镜像运行状态枚举 + * @date 2020-07-15 + **/ +public enum ImageStateEnum { + + MAKING(0, "制作中"), + SUCCESS(1, "制作成功"), + FAIL(2, "制作失败"); + + + /** + * 编码 + */ + private Integer code; + + /** + * 描述 + */ + private String description; + + ImageStateEnum(int code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageTypeEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageTypeEnum.java new file mode 100644 index 0000000..9bd9e26 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ImageTypeEnum.java @@ -0,0 +1,97 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.enums; + + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @description 镜像项目枚举类 + * @date 2020-12-11 + */ +@Getter +public enum ImageTypeEnum { + + /** + * notebook镜像 + */ + NOTEBOOK("notebook镜像", "notebook", 0), + + /** + * 训练镜像 + */ + TRAIN("训练镜像", "train", 1), + + /** + * Serving镜像 + */ + SERVING("Serving镜像", "serving", 2); + + /** + * 镜像项目名称 + */ + private String name; + /** + * 镜像项目代码 + */ + private String code; + /** + * 镜像项目类型 + */ + private Integer type; + + ImageTypeEnum(String name, String code, Integer type) { + this.name = name; + this.code = code; + this.type = type; + } + + private static final Map RESOURCE_ENUM_MAP = new HashMap() { + { + for (ImageTypeEnum enums : ImageTypeEnum.values()) { + put(enums.getType(), enums); + } + } + }; + + /** + * 根据type获取ImageTypeEnum + * @param type + * @return 镜像项目枚举对象 + */ + public static ImageTypeEnum getType(int type) { + return RESOURCE_ENUM_MAP.get(type); + } + + /** + * 根据type获取code + * + * @param type 镜像项目类型 + * @return String 镜像项目代码 + */ + public static String getType(Integer type) { + for (ImageTypeEnum typeEnum : ImageTypeEnum.values()) { + if (typeEnum.getType().equals(type)) { + return typeEnum.getCode(); + } + } + return null; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/MeasureStateEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/MeasureStateEnum.java new file mode 100644 index 0000000..6254440 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/MeasureStateEnum.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +/** + * @description 度量文件生成状态 + * @date 2020-07-15 + **/ +public enum MeasureStateEnum { + + MAKING(0, "生成中"), + SUCCESS(1, "生成成功"), + FAIL(2, "生成失败"); + + + /** + * 编码 + */ + private Integer code; + + /** + * 描述 + */ + private String description; + + MeasureStateEnum(int code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ModelResourceEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ModelResourceEnum.java new file mode 100644 index 0000000..8fb80f6 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ModelResourceEnum.java @@ -0,0 +1,66 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +import lombok.Getter; + +/** + * @description 模型资源枚举类 + * @date 2020-11-19 + */ +@Getter +public enum ModelResourceEnum { + + /** + * 我的模型 + */ + MINE(0, "我的模型"), + /** + * 预置模型 + */ + PRESET(1, "预置模型"), + /** + * 炼知模型 + */ + ATLAS(2, "炼知模型"); + + private Integer type; + + private String description; + + ModelResourceEnum(Integer type, String description) { + this.type = type; + this.description = description; + } + + /** + * 根据类型获取枚举类对象 + * + * @param type 类型 + * @return 枚举类对象 + */ + public static ModelResourceEnum getType(Integer type) { + for (ModelResourceEnum modelResourceEnum : values()) { + if (modelResourceEnum.getType().compareTo(type) == 0) { + //获取指定的枚举 + return modelResourceEnum; + } + } + return null; + } +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/OperationTypeEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/OperationTypeEnum.java new file mode 100644 index 0000000..7061dd0 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/OperationTypeEnum.java @@ -0,0 +1,69 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +import lombok.Getter; + +/** + * @description 操作类型枚举 + * @date 2020-11-25 + */ +@Getter +public enum OperationTypeEnum { + /** + * SELECT 查询类型 + */ + SELECT("select", "查询"), + + /** + * UPDATE 修改类型 + */ + UPDATE("update", "修改"), + + /** + * DELETE 删除类型 + */ + DELETE("delete", "删除"), + + /** + * LIMIT 禁止操作类型 + */ + LIMIT("limit", "禁止操作"), + + /** + * INSERT 新增类型 + */ + INSERT("insert", "新增类型"), + + ; + + /** + * 操作类型值 + */ + private String type; + + /** + * 操作类型备注 + */ + private String desc; + + OperationTypeEnum(String type, String desc) { + this.type = type; + this.desc = desc; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ResourcesPoolTypeEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ResourcesPoolTypeEnum.java new file mode 100644 index 0000000..582d85a --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/ResourcesPoolTypeEnum.java @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.enums; + +/** + * @description 规格类型 + * @date 2020-07-15 + */ +public enum ResourcesPoolTypeEnum { + + CPU(0, "CPU"), + GPU(1, "GPU"); + + + /** + * 编码 + */ + private Integer code; + + /** + * 描述 + */ + private String description; + + ResourcesPoolTypeEnum(int code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 是否是GPU编码 + * @param code + * @return true 是 ,false 否 + */ + public static boolean isGpuCode(Integer code){ + return GPU.getCode().equals(code); + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SwitchEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SwitchEnum.java new file mode 100644 index 0000000..49c0505 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SwitchEnum.java @@ -0,0 +1,95 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.enums; + +/** + * @description 是否开关枚举 + * @date 2020-06-01 + */ +public enum SwitchEnum { + /** + * OFF 否 + */ + OFF(0, "否"), + + /** + * ON 否 + */ + ON(1, "是"), + + ; + + private Integer value; + + private String desc; + + SwitchEnum(Integer value, String desc) { + this.value = value; + this.desc = desc; + } + + public Integer getValue() { + return this.value; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public static SwitchEnum getEnumValue(Integer value) { + switch (value) { + case 0: + return OFF; + case 1: + return ON; + default: + return OFF; + } + } + + public static Boolean getBooleanValue(Integer value) { + switch (value) { + case 1: + return true; + case 0: + return false; + default: + return false; + } + } + + public static boolean isExist(Integer value) { + for (SwitchEnum itm : SwitchEnum.values()) { + if (value.compareTo(itm.getValue()) == 0) { + return true; + } + } + return false; + } + + + @Override + public String toString() { + return "[" + this.value + "]" + this.desc; + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SystemNodeEnum.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SystemNodeEnum.java new file mode 100644 index 0000000..442edec --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/enums/SystemNodeEnum.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.enums; + +/** + * @description 节点枚举类 + * @date 2020-07-08 + */ +public enum SystemNodeEnum { + /** + * 网络资源 + */ + NETWORK("NetworkUnavailable", "网络资源不足"), + /** + * 内存资源 + */ + MEMORY("MemoryPressure", "内存资源不足"), + /** + * 磁盘资源 + */ + DISK("DiskPressure", "磁盘资源不足"), + /** + * 进程资源 + */ + PROCESS("PIDPressure", "进程资源不足"); + + private String type; + private String message; + + SystemNodeEnum(String type, String message) { + this.type = type; + this.message = message; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + /** + * 根据type查询message信息 + * + * @param systemNodeType 节点类型 + * @return String message信息 + */ + public static String findMessageByType(String systemNodeType) { + SystemNodeEnum[] systemNodeEnums = SystemNodeEnum.values(); + for (SystemNodeEnum systemNodeEnum : systemNodeEnums) { + if (systemNodeType.equals(systemNodeEnum.getType())) { + return systemNodeEnum.getMessage(); + } + } + return null; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/BusinessException.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/BusinessException.java new file mode 100644 index 0000000..72da9af --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/BusinessException.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.exception; + +import lombok.Getter; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.vo.DataResponseBody; + +/** + * @description 业务异常 + * @date 2020-11-26 + */ +@Getter +public class BusinessException extends RuntimeException { + + private DataResponseBody responseBody; + + public BusinessException(String msg) { + super(msg); + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST, msg); + } + + public BusinessException(String msg, Throwable cause) { + super(msg,cause); + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST, msg); + } + + public BusinessException(Throwable cause) { + super(cause); + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST); + } + + public BusinessException(Integer code, String msg, String info, Throwable cause) { + super(msg,cause); + if (info == null) { + this.responseBody = new DataResponseBody(code, msg); + } else { + this.responseBody = new DataResponseBody(code, msg + ":" + info); + } + } + + public BusinessException(ErrorCode errorCode, Throwable cause) { + this(errorCode.getCode(), errorCode.getMsg(), null, cause); + } + + public BusinessException(ErrorCode errorCode, String info, Throwable cause) { + this(errorCode.getCode(), errorCode.getMsg(), info, cause); + } + + public BusinessException(ErrorCode errorCode) { + this(errorCode, null); + } + + public BusinessException(Integer code,String msg) { + this.responseBody = new DataResponseBody(code, msg); + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/CaptchaException.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/CaptchaException.java new file mode 100644 index 0000000..c98496e --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/CaptchaException.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.exception; + +import lombok.Getter; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.vo.DataResponseBody; + +/** + * @description 验证码异常 + * @date 2020-02-23 + */ +@Getter +public class CaptchaException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private DataResponseBody responseBody; + private Throwable cause; + + public CaptchaException(String msg) { + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST, msg); + } + + public CaptchaException(String msg, Throwable cause) { + this.cause = cause; + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST, msg); + } + + public CaptchaException(Throwable cause) { + this.cause = cause; + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST); + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/DataSequenceException.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/DataSequenceException.java new file mode 100644 index 0000000..846270b --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/DataSequenceException.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.exception; + +import lombok.Getter; +import org.dubhe.biz.base.exception.BusinessException; + +/** + * @description 获取序列异常 + * @date 2020-09-23 + */ +@Getter +public class DataSequenceException extends BusinessException { + + private static final long serialVersionUID = 1L; + + public DataSequenceException(String msg) { + super(msg); + } + + public DataSequenceException(String msg, Throwable cause) { + super(msg,cause); + } + + public DataSequenceException(Throwable cause) { + super(cause); + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/ErrorCode.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/ErrorCode.java new file mode 100644 index 0000000..579dff7 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/ErrorCode.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.exception; + + +/** + * @description 异常code + * @date 2020-11-26 + */ +public interface ErrorCode { + + /** + * 错误码 + * @return code + */ + Integer getCode(); + + /** + * error info + * @return + */ + String getMsg(); + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/FeignException.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/FeignException.java new file mode 100644 index 0000000..a47c1af --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/FeignException.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.exception; + +import lombok.Getter; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.vo.DataResponseBody; + +/** + * @description 远程调用异常 + * @date 2020-11-26 + */ +@Getter +public class FeignException extends RuntimeException { + + private DataResponseBody responseBody; + + public FeignException(String msg) { + super(msg); + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST, msg); + } + + public FeignException(String msg, Throwable cause) { + super(msg,cause); + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST, msg); + } + + public FeignException(Throwable cause) { + super(cause); + this.responseBody = new DataResponseBody(ResponseCode.BADREQUEST); + } + + public FeignException(Integer code, String msg, String info, Throwable cause) { + super(msg,cause); + if (info == null) { + this.responseBody = new DataResponseBody(code, msg); + } else { + this.responseBody = new DataResponseBody(code, msg + ":" + info); + } + } + + public FeignException(ErrorCode errorCode, Throwable cause) { + this(errorCode.getCode(), errorCode.getMsg(), null, cause); + } + + public FeignException(ErrorCode errorCode, String info, Throwable cause) { + this(errorCode.getCode(), errorCode.getMsg(), info, cause); + } + + public FeignException(ErrorCode errorCode) { + this(errorCode, null); + } + + public FeignException(Integer code, String msg) { + this.responseBody = new DataResponseBody(code, msg); + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/OAuthResponseError.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/OAuthResponseError.java new file mode 100644 index 0000000..dbfb17a --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/exception/OAuthResponseError.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.exception; + +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.http.HttpStatus; + +/** + * @description OAuth2 授权自定义异常 + * @date 2020-11-18 + */ +public class OAuthResponseError extends RuntimeException{ + + private DataResponseBody responseBody; + + private HttpStatus statusCode; + + public OAuthResponseError(HttpStatus statusCode, String msg) { + super(msg); + this.statusCode = statusCode; + this.responseBody = new DataResponseBody(statusCode.value(),msg,null); + } + + public HttpStatus getStatusCode() { + return statusCode; + } + + public DataResponseBody getResponseBody() { + return responseBody; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/functional/StringFormat.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/functional/StringFormat.java new file mode 100644 index 0000000..1b64e95 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/functional/StringFormat.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.functional; + +/** + * @description 字符串格式化函数式接口 + * @date 2021-02-03 + */ +@FunctionalInterface +public interface StringFormat { + /** + * 格式化对象为字符串 + * @param value 被格式对象 + * @return + */ + String format(Object value); +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/service/UserContextService.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/service/UserContextService.java new file mode 100644 index 0000000..7e16c80 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/service/UserContextService.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.service; + +import org.dubhe.biz.base.context.UserContext; + +/** + * @description 获取用户上下文接口 + * @date 2020-12-07 + */ +public interface UserContextService { + + /** + * @return 用户上下文信息 + */ + UserContext getCurUser(); + + /** + * @return 用户ID + */ + Long getCurUserId(); + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/AesUtil.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/AesUtil.java new file mode 100644 index 0000000..da90596 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/AesUtil.java @@ -0,0 +1,94 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import cn.hutool.core.util.HexUtil; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @description AES加解密工具 + * @date 2020-06-01 + */ +public class AesUtil { + + private static final String AES = "AES"; + + + private AesUtil(){ + + } + + + /** + * + * @param mode Cipher mode + * @param key 秘钥 + * @return Cipher + * @throws NoSuchAlgorithmException + * @throws NoSuchPaddingException + * @throws InvalidKeyException + */ + private static Cipher getCipher(int mode,String key) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { + MessageDigest md5Digest = MessageDigest.getInstance("MD5"); + SecretKeySpec secretKeySpec = new SecretKeySpec(md5Digest.digest(key.getBytes(StandardCharsets.UTF_8)), AES); + Cipher cipher = Cipher.getInstance(AES); + cipher.init(mode, secretKeySpec); + return cipher; + } + + /** + * 加密 + * + * @param data 原文 + * @param key 秘钥 + * @return String 密文 + */ + public static String encrypt(String data, String key) { + try { + Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,key); + byte[] content = data.getBytes(StandardCharsets.UTF_8); + return new String(HexUtil.encodeHex(cipher.doFinal(content), false)); + } catch (Exception e) { + return null; + } + } + + /** + * 解密 + * @param hexData 十六进制密文 + * @param key 秘钥 + * @return String 密文 + */ + public static String decrypt(String hexData, String key) { + try { + Cipher cipher = getCipher(Cipher.DECRYPT_MODE,key); + byte[] content = HexUtil.decodeHex(hexData); + return new String(cipher.doFinal(content), StandardCharsets.UTF_8); + } catch (Exception e) { + return null; + } + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/DateUtil.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/DateUtil.java new file mode 100644 index 0000000..63d2e14 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/DateUtil.java @@ -0,0 +1,128 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * @description 日期工具类 + * @date 2020-11-26 + */ +public class DateUtil { + + private DateUtil() { + + } + + /** + * 北京时间 + */ + public final static String DEFAULT_TIME_ZONE = "GMT+8"; + + + /** + * 获取当前时间戳 + * + * @return + */ + public static Timestamp getCurrentTimestamp() { + return Timestamp.valueOf(LocalDateTime.now()); + } + + + /** + * 获取六小时后时间 + * @return + */ + public static long getAfterSixHourTime() { + long l1 = getTimestampOfDateTime(LocalDateTime.now()); + long milli = getTimestampOfDateTime(LocalDateTime.now().plusHours(6)); + return (milli - l1); + } + + + /** + * LocalDateTime -> long + * @param localDateTime + * @return + */ + public static long getTimestampOfDateTime(LocalDateTime localDateTime) { + ZoneId zone = ZoneId.systemDefault(); + Instant instant = localDateTime.atZone(zone).toInstant(); + return instant.toEpochMilli(); + } + + /** + * 获取第二天凌晨时间 + * @return + */ + public static long getSecondTime() { + LocalDateTime localDateTime = LocalDateTime.now(); + long l1 = localDateTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli(); + + LocalDate localDate = LocalDate.now(); + LocalDate localDate1 = localDate.plusDays(1); + LocalDateTime localDateTime1 = localDate1.atStartOfDay(); + long milli = localDateTime1.atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli(); + return (milli - l1); + } + + /** + * @return 当前字符串时间yyyy-MM-dd HH:mm:ss SSS + */ + public static String getCurrentTimeStr() { + Date date = new Date(); + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); + return dateFormat.format(date); + } + + /** + * + * @return 当前字符串时间yyyyMMddHHmmss + */ + public static String getTimestampStr() { + SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS"); + return df.format(System.currentTimeMillis()); + } + + + /** + * 获取垃圾回收具体的预执行时间 + * + * @param afterDay 延迟执行天数 + * @return Timestamp 具体的执行时间 + */ + public static Timestamp getRecycleTime(int afterDay){ + + Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + calendar.add(Calendar.DATE, afterDay); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd 01:00:00"); + + return Timestamp.valueOf(sdf.format(calendar.getTime())); + } +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/HttpUtils.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/HttpUtils.java new file mode 100644 index 0000000..b604ab5 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/HttpUtils.java @@ -0,0 +1,66 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import lombok.extern.slf4j.Slf4j; + +/** + * @description HttpUtil + * @date 2020-04-30 + */ +@Slf4j +public class HttpUtils { + + private HttpUtils() { + + } + + /** + * 判断http请求是否成功 + * + * @param httpCode + * 1XX Informational(信息性状态码) + * 2XX Success(成功状态码) + * 3XX Redirection(重定向状态码) + * 4XX Client Error(客户端错误状态码) + * 5XX Server Error(服务器错误状态码) + * @return + */ + public static boolean isSuccess(String httpCode){ + if (StringUtils.isBlank(httpCode)){ + return false; + } + return httpCode.length() == 3 && httpCode.startsWith("2"); + } + + /** + * 判断http请求是否成功 + * + * @param httpCode + * 1XX Informational(信息性状态码) + * 2XX Success(成功状态码) + * 3XX Redirection(重定向状态码) + * 4XX Client Error(客户端错误状态码) + * 5XX Server Error(服务器错误状态码) + * @return + */ + public static boolean isSuccess(int httpCode) { + return isSuccess(String.valueOf(httpCode)); + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/MathUtils.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/MathUtils.java new file mode 100644 index 0000000..2d779c5 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/MathUtils.java @@ -0,0 +1,75 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + + +import org.dubhe.biz.base.constant.MagicNumConstant; + +/** + * @description 计算工具类 + * @date 2020-06-04 + */ +public class MathUtils { + + private static String FLOAT_DIVISION_FORMATE = "%1$0"; + + /** + * 字符串整数加法 + * num1+num2 + * @param num1 + * @param num2 + * @return + */ + public static String add(String num1, String num2) { + return String.valueOf((!RegexUtil.isDigits(num1) ? MagicNumConstant.ZERO : Integer.valueOf(num1)) + (!RegexUtil.isDigits(num2) ? MagicNumConstant.ZERO : Integer.valueOf(num2))); + } + + /** + * 字符串整数减法 + * num1 - num2 + * @param num1 + * @param num2 + * @return + */ + public static String reduce(String num1, String num2) { + return String.valueOf((!RegexUtil.isDigits(num1) ? MagicNumConstant.ZERO : Integer.valueOf(num1)) - (!RegexUtil.isDigits(num2) ? MagicNumConstant.ZERO : Integer.valueOf(num2))); + } + + /** + * + * 浮点数除法 num1/num2 + * @param num1 + * @param num2 + * @param decimal 结果小数位数 + * @return + */ + public static Float floatDivision(String num1, String num2, Integer decimal) { + if (!RegexUtil.isFloat(num1) || !RegexUtil.isFloat(num2)) { + return null; + } + if (Float.valueOf(num2).equals(0f)) { + return null; + } + if (decimal != null && decimal > MagicNumConstant.ZERO) { + Integer d = Integer.valueOf(MagicNumConstant.ONE + String.format(FLOAT_DIVISION_FORMATE + decimal + "d", MagicNumConstant.ZERO)); + return (float) (Math.round((Float.valueOf(num1) / Float.valueOf(num2)) * d)) / d; + } else { + return Float.valueOf(num1) / Float.valueOf(num2); + } + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/Md5Util.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/Md5Util.java new file mode 100644 index 0000000..a9f5437 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/Md5Util.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import java.security.MessageDigest; + +/** + * @description md5工具类 + * @date 2020-06-29 + */ +public class Md5Util { + public static final String CHARSET = "UTF-8"; + + public final static String createMd5(String s) { + char[] hexDigits ={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; + try { + byte[] btInput = s.getBytes(); + // 获得MD5摘要算法的 MessageDigest 对象 + MessageDigest mdInst = MessageDigest.getInstance("MD5"); + // 使用指定的字节更新摘要 + mdInst.update(btInput); + // 获得密文 + byte[] md = mdInst.digest(); + // 把密文转换成十六进制的字符串形式 + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + for (int i = 0; i < j; i++) { + byte byte0 = md[i]; + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + return null; + } + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/NumberUtil.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/NumberUtil.java new file mode 100644 index 0000000..21722be --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/NumberUtil.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import org.dubhe.biz.base.exception.BusinessException; + +import java.util.regex.Pattern; + +/** + * @description 数字验证工具 + * @date 2020-05-18 + */ +public class NumberUtil { + + private static final String REGEX = "^[0-9]*$"; + + private NumberUtil() { + + } + + /** + * 判断是否为数字格式不限制位数 + * + * @param object 待校验参数 + */ + public static void isNumber(Object object) { + if (!((Pattern.compile(REGEX)).matcher(String.valueOf(object)).matches())) { + throw new BusinessException("parameter is incorrect"); + } + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/PtModelUtil.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/PtModelUtil.java new file mode 100644 index 0000000..8a521a6 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/PtModelUtil.java @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +/** + * @description 模型管理工具类 + * @date 2020-07-17 + */ +public class PtModelUtil { + public static final int NUMBER_ZERO = 0; + + public static final int NUMBER_ONE = 1; + + public static final int NUMBER_TWO = 2; + + public static final int NUMBER_THREE = 3; + + public static final int NUMBER_FOUR = 4; + + public static final int NUMBER_FIVE = 5; + + public static final int NUMBER_EIGHT = 8; + + public static final int NUMBER_ONE_HUNDRED_TWENTY_EIGHT = 128; + + public static final int NUMBER_TWO_HUNDRED_FIFTY_FIVE = 255; + + public static final String ZIP = ".zip"; + + public static final String SORT_ASC = "asc"; + + public static final String SORT_DESC = "desc"; + + public static final String ID = "id"; + + public static final int USER_UPLOAD = 0; + + public static final int TRAINING_IMPORT = 1; + + public static final int MODEL_OPTIMIZATION = 2; + + public static final int RANDOM_LENGTH = 4; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RandomUtil.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RandomUtil.java new file mode 100644 index 0000000..66e25a4 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RandomUtil.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + + +/** + * @description 随机数工具类 + * @date 2020-06-01 + */ +public class RandomUtil { + + /** + * 生成随机6位数 + * + * @return 6位随机数 + */ + public static String randomCode() { + Integer res = (int) ((Math.random()) * 1000000); + return res + ""; + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/ReflectionUtils.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/ReflectionUtils.java new file mode 100644 index 0000000..dfa330c --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/ReflectionUtils.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * @description 反射工具类 + * @date 2020-05-29 + **/ +public class ReflectionUtils { + + /** + * 获取所有字段集合 + * + * @param clazz 反射对象 + * @return List 字段集合 + **/ + public static List getFieldNames(Class clazz) { + Field[] fields = clazz.getDeclaredFields(); + List fieldNameList = new ArrayList<>(); + for (Field field : fields) { + fieldNameList.add(field.getName()); + } + return fieldNameList; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RegexUtil.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RegexUtil.java new file mode 100644 index 0000000..99d00da --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RegexUtil.java @@ -0,0 +1,76 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @description 正则匹配工具类 + * @date 2020-04-23 + */ +@Slf4j +public class RegexUtil { + private static final String DIGIT = "^[0-9]*$"; + private static final String FLOAT = "^[-+]?[0-9]*\\.?[0-9]+$"; + /** + * str待匹配文本 + * regex 正则表达式 + *返回str中匹配regex的第一个子串 + */ + public static String getMatcher(String str,String regex) { + try{ + if (StringUtils.isEmpty(str) || StringUtils.isEmpty(regex)){ + return ""; + } + Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = p.matcher(str); + matcher.find(); + return matcher.group(); + }catch (IllegalStateException e){ + log.error(e.getMessage(), e); + return ""; + } + } + + /** + * 数字匹配 + * @param str + * @return + */ + public static boolean isDigits(String str){ + if (StringUtils.isEmpty(str)){ + return false; + } + return str.matches(DIGIT); + } + + /** + * 浮点数匹配 + * @param str + * @return + */ + public static boolean isFloat(String str){ + if (StringUtils.isEmpty(str)){ + return false; + } + return str.matches(FLOAT); + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RsaEncrypt.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RsaEncrypt.java new file mode 100644 index 0000000..99f0510 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/RsaEncrypt.java @@ -0,0 +1,150 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; + +/** + * @description RSA对称加密工具 + * @date 2020-06-01 + */ +@Slf4j +public class RsaEncrypt { + /** + * 指定加密算法为RSA + */ + private static final String ALGORITHM = "RSA"; + /** + * 密钥长度,用来初始化 + */ + private static final int KEYSIZE = 1024; + /** + * 指定公钥存放文件 + */ + private static String PUBLIC_KEY_FILE = "PublicKey"; + /** + * 指定私钥存放文件 + */ + private static String PRIVATE_KEY_FILE = "PrivateKey"; + + /** + * 用于封装随机产生的公钥与私钥 + */ + + + /** + * 随机生成密钥对 + * + * @throws NoSuchAlgorithmException + */ + public static Map genKeyPair() throws NoSuchAlgorithmException { + // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(ALGORITHM); + // 初始化密钥对生成器,密钥大小为96-1024位 + keyPairGen.initialize(KEYSIZE, new SecureRandom()); + // 生成一个密钥对,保存在keyPair中 + KeyPair keyPair = keyPairGen.generateKeyPair(); + /*得到私钥*/ + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // 得到公钥 + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded())); + // 得到私钥字符串 + String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded()))); + Map keyMap = new HashMap(2); + // 将公钥和私钥保存到Map + keyMap.put(PUBLIC_KEY_FILE, publicKeyString); + keyMap.put(PRIVATE_KEY_FILE, privateKeyString); + return keyMap; + } + + /** + * 获取公钥 + * + * @param rasKeys + * @return + */ + public static String getPublicKey(Map rasKeys) { + return rasKeys.get(PUBLIC_KEY_FILE); + } + + /** + * 获取私钥 + * + * @param rasKeys + * @return + */ + public static String getPrivateKey(Map rasKeys) { + return rasKeys.get(PRIVATE_KEY_FILE); + } + + /** + * RSA公钥加密 + * + * @param str 加密字符串 + * @param publicKey 公钥 + * @return 密文 + * @throws Exception 加密过程中的异常信息 + */ + public static String encrypt(String str, String publicKey) throws Exception { + //base64编码的公钥 + byte[] decoded = Base64.decodeBase64(publicKey); + RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(decoded)); + //RSA加密 + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + String outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8"))); + return outStr; + } + + /** + * RSA私钥解密 + * + * @param str 加密字符串 + * @param privateKey 私钥 + * @return 铭文 + * @throws Exception 解密过程中的异常信息 + */ + public static String decrypt(String str, String privateKey) { + String outStr = null; + try { + //64位解码加密后的字符串 + byte[] inputByte = Base64.decodeBase64(str.getBytes("UTF-8")); + //base64编码的私钥 + byte[] decoded = Base64.decodeBase64(privateKey); + RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(decoded)); + //RSA解密 + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, priKey); + outStr = new String(cipher.doFinal(inputByte)); + } catch (Exception e) { + log.error("RSAEncrypt decrypt error:{} ", e); + } + return outStr; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/SpringContextHolder.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/SpringContextHolder.java new file mode 100644 index 0000000..7c19c3d --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/SpringContextHolder.java @@ -0,0 +1,94 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * @description 上下文工具类 + * @date 2020-03-25 + */ +@Slf4j +@Component +public class SpringContextHolder implements ApplicationContextAware, DisposableBean { + + private static ApplicationContext applicationContext = null; + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + assertContextInjected(); + return (T) applicationContext.getBean(name); + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + assertContextInjected(); + return applicationContext.getBean(requiredType); + } + + /** + * 检查ApplicationContext不为空. + */ + private static void assertContextInjected() { + if (applicationContext == null) { + throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" + + ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder."); + } + } + + /** + * 清除SpringContextHolder中的ApplicationContext为Null. + */ + private static void clearHolder() { + log.debug("清除SpringContextHolder中的ApplicationContext:" + + applicationContext); + applicationContext = null; + } + + @Override + public void destroy() { + SpringContextHolder.clearHolder(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringContextHolder.applicationContext != null) { + log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext); + } + SpringContextHolder.applicationContext = applicationContext; + } + + /** + * 获取当前环境 + * + * @return + */ + public static String getActiveProfile(){ + return applicationContext.getEnvironment().getActiveProfiles()[0]; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/StringUtils.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/StringUtils.java new file mode 100644 index 0000000..9905cfd --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/StringUtils.java @@ -0,0 +1,417 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import com.alibaba.fastjson.JSON; +import eu.bitwalker.useragentutils.Browser; +import eu.bitwalker.useragentutils.UserAgent; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.constant.SymbolConstant; + +import javax.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @description 字符串工具类, 继承org.apache.commons.lang3.StringUtils类 + * @date 2020-03-25 + */ +@Slf4j +public class StringUtils extends org.apache.commons.lang3.StringUtils { + + private static final char SEPARATOR = '_'; + + private static final String UNKNOWN = "unknown"; + + private static Pattern linePattern = Pattern.compile("_(\\w)"); + + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + + s = s.toLowerCase(); + + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCapitalizeCamelCase(String s) { + if (s == null) { + return null; + } + s = toCamelCase(s); + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + static String toUnderScoreCase(String s) { + if (s == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + boolean nextUpperCase = true; + + if (i < (s.length() - 1)) { + nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); + } + + if ((i > 0) && Character.isUpperCase(c)) { + if (!upperCase || !nextUpperCase) { + sb.append(SEPARATOR); + } + upperCase = true; + } else { + upperCase = false; + } + + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + + /** + * 获取ip地址 + * + * @param request + * @return + */ + public static String getIp(HttpServletRequest request) { + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + String comma = ","; + String localhost = "127.0.0.1"; + if (ip.contains(comma)) { + ip = ip.split(",")[0]; + } + if (localhost.equals(ip)) { + // 获取本机真正的ip地址 + try { + ip = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + return ip; + } + + public static String getBrowser(HttpServletRequest request) { + UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); + Browser browser = userAgent.getBrowser(); + return browser.getName(); + } + + + /** + * 获得当天是周几 + */ + public static String getWeekDay() { + String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + + int w = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (w < 0) { + w = 0; + } + return weekDays[w]; + } + + /** + * 字符串匹配工具类(正则表达式) + * + * @param str + * @param regexp + * @return + */ + public static boolean stringMatch(String str, String regexp) { + Pattern pattern = Pattern.compile(regexp); + return pattern.matcher(str).find(); + } + + /** + * 字符串长度不够补位 + * + * @param sourceStr 源字符串 + * @param length 最终长度 + * @param type 补位方式 0:左边 1:右边 + * @return + */ + public static String stringFillIn(String sourceStr, int length, int type) { + if (sourceStr.length() > length) { + return sourceStr; + } + StringBuilder stringBuilder = new StringBuilder(""); + for (int i = 0; i < length - sourceStr.length(); i++) { + stringBuilder.append("0"); + } + + if (type == MagicNumConstant.ZERO) { + return stringBuilder.toString() + sourceStr; + } else { + return sourceStr + stringBuilder.toString(); + } + } + + + /** + * 从头截取string字符串 + * + * @param str 被截取字符串 + * @param truncationIndex 0 -> 截取长度 + * @return + */ + public static String truncationString(String str, int truncationIndex) { + if (str == null) { + return SymbolConstant.BLANK; + } else if (truncationIndex < 1 + || str.length() <= truncationIndex) { + return str; + } + return str.substring(0, truncationIndex); + } + + /** + * 字符串驼峰转下划线 + * + * @param str 被转字符串 + * @return String 转换后的字符串 + */ + public static String humpToLine(String str) { + StringBuilder stringBuilder = new StringBuilder(); + char[] chars = str.toCharArray(); + for (char character : chars) { + if (Character.isUpperCase(character)) { + stringBuilder.append("_"); + character = Character.toLowerCase(character); + } + stringBuilder.append(character); + } + return stringBuilder.toString(); + } + + /** + * 字符串下划线转驼峰 + * + * @param str 被转字符串 + * @return String 转换后的字符串 + */ + public static String lineToHump(String str) { + str = str.toLowerCase(); + Matcher matcher = linePattern.matcher(str); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(sb, matcher.group(1).toUpperCase()); + } + matcher.appendTail(sb); + return sb.toString(); + } + + + /** + * 字符串截取前 + * @param str + * @return + */ + public static String substringBefore(String str, String separator) { + + if (!isEmpty(str) && separator != null) { + if (separator.isEmpty()) { + return ""; + } else { + int pos = str.indexOf(separator); + return pos == -1 ? str : str.substring(0, pos); + } + } else { + return str; + } + } + + /** + * 字符串截取后 + * @param str + * @return + */ + public static String substringAfter(String str, String separator) { + + if (isEmpty(str)) { + return str; + } else if (separator == null) { + return ""; + } else { + int pos = str.indexOf(separator); + return pos == -1 ? "" : str.substring(pos + separator.length()); + } + } + + /** + * 获取UUID字符串 + * + * @return + */ + public static String getUUID() { + return UUID.randomUUID().toString().replace(SymbolConstant.HYPHEN, SymbolConstant.BLANK); + } + + /** + * 生成4位随机[a-z]字符串 + * + * @return + */ + public static String getRandomString() { + return RandomStringUtils.randomAlphabetic(NumberConstant.NUMBER_4).toLowerCase(); + } + + /** + * 生成时间戳 + 4位随机数 + * + * @return + */ + public static String getTimestamp() { + SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); + String dateStr = df.format(System.currentTimeMillis()); + return dateStr + getRandomString(); + } + + /** + * 往json字符串的map添加键值对 + * + * @param key 键 + * @param value 值 非null + * @param jsonStringMap json字符串的map + * @return + */ + public static String putIntoJsonStringMap(String key, String value, String jsonStringMap) { + if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) { + return jsonStringMap; + } + try { + if (StringUtils.isEmpty(jsonStringMap)) { + jsonStringMap = JSON.toJSONString(new HashMap()); + } + Map map = JSON.parseObject(jsonStringMap, Map.class); + map.put(key, value); + return JSON.toJSONString(map); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return jsonStringMap; + } + + /** + * 删除json字符串的键值对 + * + * @param key 键 + * @param jsonStringMap json字符串的map + * @return + */ + public static String removeFromJsonStringMap(String key, String jsonStringMap) { + if (StringUtils.isEmpty(key) || StringUtils.isEmpty(jsonStringMap)) { + return jsonStringMap; + } + try { + Map map = JSON.parseObject(jsonStringMap, Map.class); + map.remove(key); + return JSON.toJSONString(map); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return jsonStringMap; + } + + /** + * 从json字符串的map中获取value + * + * @param key 键 + * @param jsonStringMap json字符串的map + * @return value + */ + public static String getValueFromJsonStringMap(String key, String jsonStringMap) { + if (StringUtils.isEmpty(key) || StringUtils.isEmpty(jsonStringMap)) { + return null; + } + try { + Map map = JSON.parseObject(jsonStringMap, Map.class); + return map.get(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/TimeTransferUtil.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/TimeTransferUtil.java new file mode 100644 index 0000000..a602ee8 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/utils/TimeTransferUtil.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.utils; + +import org.dubhe.biz.base.constant.MagicNumConstant; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +/** + * @description 时间格式转换工具类 + * @date 2020-05-20 + */ +public class TimeTransferUtil { + + private static final String UTC_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.sss'Z'"; + + /** + * Date转换为UTC时间 + * + * @param date + * @return utcTime + */ + public static String dateTransferToUtc(Date date){ + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + /**UTC时间与CST时间相差8小时**/ + calendar.set(Calendar.HOUR,calendar.get(Calendar.HOUR) - MagicNumConstant.EIGHT); + SimpleDateFormat utcSimpleDateFormat = new SimpleDateFormat(UTC_FORMAT); + Date utcDate = calendar.getTime(); + return utcSimpleDateFormat.format(utcDate); + } +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/BaseVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/BaseVO.java new file mode 100644 index 0000000..ef2b0b6 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/BaseVO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description VO基础类 + * @date 2020-05-22 + */ +@Data +public class BaseVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long createUserId; + + private Timestamp createTime; + + private Long updateUserId; + + private Timestamp updateTime; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DataResponseBody.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DataResponseBody.java new file mode 100644 index 0000000..733f15a --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DataResponseBody.java @@ -0,0 +1,78 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + + +import lombok.Data; +import org.dubhe.biz.base.constant.ResponseCode; +import org.slf4j.MDC; + +import java.io.Serializable; + +/** + * @description 统一的公共响应体 + * @date 2020-03-16 + */ +@Data +public class DataResponseBody implements Serializable { + + /** + * 返回状态码 + */ + private Integer code; + /** + * 返回信息 + */ + private String msg; + /** + * 泛型数据 + */ + private T data; + /** + * 链路追踪ID + */ + private String traceId; + + public DataResponseBody() { + this(ResponseCode.SUCCESS, null); + } + + public DataResponseBody(T data) { + this(ResponseCode.SUCCESS, null, data); + } + + public DataResponseBody(Integer code, String msg) { + this(code, msg, null); + } + + public DataResponseBody(Integer code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + this.traceId = MDC.get("traceId"); + } + + /** + * 判断是否响应成功 + * @return ture 成功,false 失败 + */ + public boolean succeed(){ + return ResponseCode.SUCCESS.equals(this.code); + } + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DatasetVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DatasetVO.java new file mode 100644 index 0000000..14ff331 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DatasetVO.java @@ -0,0 +1,160 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import lombok.Builder; +import lombok.Data; +import org.dubhe.biz.base.dto.TeamSmallDTO; +import org.dubhe.biz.base.dto.UserSmallDTO; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 数据集VO + * @date 2020-04-10 + */ +@Data +public class DatasetVO implements Serializable { + + /** + * 数据集ID + */ + private Long id; + + /** + * 数据集名称 + */ + private String name; + + /** + * 备注 + */ + private String remark; + + /** + * 类型 + */ + private Integer type; + + /** + * 数据集文件主目录 + */ + private String uri; + + /** + * 数据类型 + */ + private Integer dataType; + + /** + * 标注类型 + */ + private Integer annotateType; + + /** + * 数据集状态 + */ + private Integer status; + + /** + * 创建时间 + */ + private Timestamp createTime; + + /** + * 更新时间 + */ + private Timestamp updateTime; + + /** + * 团队信息 + */ + private TeamSmallDTO team; + + /** + * 创建人 + */ + private UserSmallDTO createUser; + + /** + * 更新人 + */ + private UserSmallDTO updateUser; + + /** + * 进度 + */ + private ProgressVO progress; + + /** + * 当前版本 + */ + private String currentVersionName; + + /** + * 是否导入 + */ + private boolean isImport; + + /** + * 解压状态 + */ + private Integer decompressState; + + /** + * 是否置顶 + */ + private boolean isTop; + + /** + * 标签组ID + */ + private Long labelGroupId; + + /** + * 标签组名称 + */ + private String labelGroupName; + + /** + * 标签组类型 + */ + private Integer labelGroupType; + + /** + * 是否自动标注 + */ + private boolean autoAnnotation; + + /** + * 数据转换状态 + */ + private Integer dataConversion; + + /** + * 源ID + */ + private Long sourceId; + + /** + * 文件数量 + */ + private Integer fileCount; + +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictDetailVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictDetailVO.java new file mode 100644 index 0000000..9de91ac --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictDetailVO.java @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 字典详情 + * @date 2020-12-23 + */ +@Data +public class DictDetailVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 字典详情id + */ + private Long id; + + /** + * 字典id + */ + private Long dictId; + + /** + * 字典标签 + */ + private String label; + + /** + * 字典值 + */ + private String value; + + /** + * 排序 + */ + private String sort; + + /** + * 创建时间 + */ + private Timestamp createTime; + + /** + * 修改时间 + */ + private Timestamp updateTime; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictVO.java new file mode 100644 index 0000000..bd5ba52 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/DictVO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + * @description 字典 + * @date 2021-01-19 + */ +@Data +public class DictVO implements Serializable { + + private static final long serialVersionUID = -1176729960392375726L; + private Long id; + + private String name; + + private String remark; + + private List dictDetails; + + private Timestamp createTime; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ModelOptAlgorithmQureyVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ModelOptAlgorithmQureyVO.java new file mode 100644 index 0000000..03d750f --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ModelOptAlgorithmQureyVO.java @@ -0,0 +1,136 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 算法详情VO + * @date 2020-04-29 + */ +@Data +@Accessors(chain = true) +public class ModelOptAlgorithmQureyVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 算法名称 + */ + private String algorithmName; + + /** + * 算法描述 + */ + private String description; + + /** + * 算法来源(1为我的算法,2为预置算法) + */ + private Integer algorithmSource; + + /** + * 环境镜像名称 + */ + private String imageName; + + /** + * 代码目录 + */ + private String codeDir; + + /** + * 运行命令 + */ + private String runCommand; + + /** + * 运行参数 + */ + private JSONObject runParams; + + /** + * 算法用途 + */ + private String algorithmUsage; + + /** + * 算法精度 + */ + private String accuracy; + + /** + * P4推理速度(ms) + */ + private Integer p4InferenceSpeed; + + /** + * 训练输出结果(1是,0否) + */ + private Boolean isTrainModelOut; + + /** + * 训练输出(1是,0否) + */ + private Boolean isTrainOut; + + /** + * 可视化日志(1是,0否) + */ + private Boolean isVisualizedLog; + + /** + * 算法状态 + */ + private Integer algorithmStatus; + + /** + * 创建人ID + */ + private Integer createUserId; + + /** + * 修改人ID + */ + private Integer updateUserId; + + /** + * 创建时间 + */ + private Timestamp createTime; + + /** + * 修改时间 + */ + private Timestamp updateTime; + + /** + * 数据拥有人ID + */ + private Integer originUserId; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ProgressVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ProgressVO.java new file mode 100644 index 0000000..02bb09f --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/ProgressVO.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.base.constant.MagicNumConstant; + +import java.io.Serializable; + +/** + * @description 数据集状态 + * @date 2020-04-10 + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@Builder +public class ProgressVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Builder.Default + private Long finished = MagicNumConstant.ZERO_LONG; + @Builder.Default + private Long unfinished = MagicNumConstant.ZERO_LONG; + @Builder.Default + private Long autoFinished = MagicNumConstant.ZERO_LONG; + @Builder.Default + private Long finishAutoTrack = MagicNumConstant.ZERO_LONG; + @Builder.Default + private Long annotationNotDistinguishFile = MagicNumConstant.ZERO_LONG; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelBranchQueryVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelBranchQueryVO.java new file mode 100644 index 0000000..25c3685 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelBranchQueryVO.java @@ -0,0 +1,130 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 版本管理删除返回对应id + * @date 2020-5-15 + */ +@Data +@Accessors(chain = true) +public class PtModelBranchQueryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 版本ID + */ + private Long id; + + /** + * 父ID + */ + private Long parentId; + + /** + * 版本号 + */ + private String version; + + /** + * 模型地址 + */ + private String modelAddress; + + /** + * 模型路径 + */ + private String modelPath; + + /** + * 模型来源(0用户上传,1训练输出,2模型优化) + */ + private Integer modelSource; + + /** + * 算法ID + */ + private Long algorithmId; + + /** + * 算法名称 + */ + private String algorithmName; + + /** + * 算法来源(1为我的算法,2为预置算法) + */ + private Integer algorithmSource; + + /** + * 文件拷贝状态(0文件拷贝中,1文件拷贝成功,2文件拷贝失败) + */ + private Integer status; + + /** + * 团队ID + */ + private Integer teamId; + + /** + * 创建人ID + */ + private Long createUserId; + + /** + * 修改人ID + */ + private Long updateUserId; + + /** + * 创建时间 + */ + private Timestamp createTime; + + /** + * 修改时间 + */ + private Timestamp updateTime; + + /** + * 数据拥有人ID + */ + private Long originUserId; + + /** + * 模型名称 + */ + private String name; + + /** + * 模型描述 + */ + private String modelDescription; + + /** + * 是否能提供服务(true:能,false:否) + */ + private Boolean servingModel; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelInfoQueryVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelInfoQueryVO.java new file mode 100644 index 0000000..5dd37d2 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/PtModelInfoQueryVO.java @@ -0,0 +1,127 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 模型管理查询返回对应信息 + * @date 2020-5-15 + */ +@Data +public class PtModelInfoQueryVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 模型ID + */ + private Long id; + + /** + * 模型名称 + */ + private String name; + + /** + * 框架类型 + */ + private Integer frameType; + + /** + * 模型文件的格式(后缀名) + */ + private Integer modelType; + + /** + * 模型描述 + */ + private String modelDescription; + + /** + * 模型分类 + */ + private String modelClassName; + + /** + * 模型地址 + */ + private String modelAddress; + + /** + * 模型版本 + */ + private String version; + + /** + * 模型是否为预置模型(0默认模型,1预置模型) + */ + private Integer modelResource; + + /** + * 模型版本总的个数 + */ + private Integer totalNum; + + /** + * 团队ID + */ + private Integer teamId; + + /** + * 创建人ID + */ + private Long createUserId; + + /** + * 修改人ID + */ + private Long updateUserId; + + /** + * 创建时间 + */ + private Timestamp createTime; + + /** + * 修改时间 + */ + private Timestamp updateTime; + + /** + * 数据拥有人ID + */ + private Long originUserId; + + /** + * 模型是否已经打包 0未打包 1 已经打包(目前仅对炼知模型) + */ + private Integer packaged; + + /** + * 模型打包tags信息 + */ + private String tags; + + /** + * 是否能提供服务(true:能,false:否) + */ + private Boolean servingModel; +} diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/QueryResourceSpecsVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/QueryResourceSpecsVO.java new file mode 100644 index 0000000..bac1c42 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/QueryResourceSpecsVO.java @@ -0,0 +1,94 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.base.vo; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 资源规格查询结果封装类 + * @date 2021-06-02 + */ +@Data +@Accessors(chain = true) +public class QueryResourceSpecsVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + private Long id; + + /** + *规格名称 + */ + private String specsName; + + /** + *规格类型(0为CPU, 1为GPU) + */ + private Boolean resourcesPoolType; + + /** + *所属业务场景 + */ + private Integer module; + + /** + *CPU数量,单位:核 + */ + private Integer cpuNum; + + /** + *GPU数量,单位:核 + */ + private Integer gpuNum; + + /** + *内存大小,单位:M + */ + private Integer memNum; + + /** + *工作空间的存储配额,单位:M + */ + private Integer workspaceRequest; + + /** + *创建人 + */ + private Long createUserId; + + /** + *创建时间 + */ + private Timestamp createTime; + + /** + *更新人 + */ + private Long updateUserId; + + /** + *更新时间 + */ + private Timestamp updateTime; +} \ No newline at end of file diff --git a/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/TrainAlgorithmQureyVO.java b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/TrainAlgorithmQureyVO.java new file mode 100644 index 0000000..83eb8c0 --- /dev/null +++ b/dubhe-server/common-biz/base/src/main/java/org/dubhe/biz/base/vo/TrainAlgorithmQureyVO.java @@ -0,0 +1,136 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.base.vo; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 算法 + * @date 2020-04-29 + */ +@Data +@Accessors(chain = true) +public class TrainAlgorithmQureyVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 算法名称 + */ + private String algorithmName; + + /** + * 算法描述 + */ + private String description; + + /** + * 算法来源(1为我的算法,2为预置算法) + */ + private Integer algorithmSource; + + /** + * 环境镜像名称 + */ + private String imageName; + + /** + * 代码目录 + */ + private String codeDir; + + /** + * 运行命令 + */ + private String runCommand; + + /** + * 运行参数 + */ + private JSONObject runParams; + + /** + * 算法用途 + */ + private String algorithmUsage; + + /** + * 算法精度 + */ + private String accuracy; + + /** + * P4推理速度(ms) + */ + private Integer p4InferenceSpeed; + + /** + * 训练输出结果(1是,0否) + */ + private Boolean isTrainModelOut; + + /** + * 训练输出(1是,0否) + */ + private Boolean isTrainOut; + + /** + * 可视化日志(1是,0否) + */ + private Boolean isVisualizedLog; + + /** + * 算法状态 + */ + private Integer algorithmStatus; + + /** + * 创建人ID + */ + private Long createUserId; + + /** + * 修改人ID + */ + private Long updateUserId; + + /** + * 创建时间 + */ + private Timestamp createTime; + + /** + * 修改时间 + */ + private Timestamp updateTime; + + /** + * 数据拥有人ID + */ + private Long originUserId; +} diff --git a/dubhe-server/common-biz/data-permission/pom.xml b/dubhe-server/common-biz/data-permission/pom.xml new file mode 100644 index 0000000..e4588a7 --- /dev/null +++ b/dubhe-server/common-biz/data-permission/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + org.dubhe.biz + common-biz + 0.0.1-SNAPSHOT + + data-permission + 0.0.1-SNAPSHOT + Biz 数据权限 + Data permission for Dubhe Server + + + + + org.dubhe.biz + db + ${org.dubhe.biz.db.version} + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + org.dubhe.cloud + swagger + ${org.dubhe.cloud.swagger.version} + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/DataPermissionMethod.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/DataPermissionMethod.java new file mode 100644 index 0000000..ea58274 --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/DataPermissionMethod.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.permission.annotation; + + +import org.dubhe.biz.base.enums.DatasetTypeEnum; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @description 数据权限方法注解 + * @date 2020-11-25 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DataPermissionMethod { + + /** + * 是否需要拦截标识 true: 不拦截 false: 拦截 + * + * @return 拦截标识 + */ + boolean interceptFlag() default false; + + /** + * 数据类型 + * + * @return 数据集类型 + */ + DatasetTypeEnum dataType() default DatasetTypeEnum.PRIVATE; +} diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/RolePermission.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/RolePermission.java new file mode 100644 index 0000000..28daf7a --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/annotation/RolePermission.java @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.permission.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.annotation.RetentionPolicy; + +/** + * @description 角色权限注解(验证是否具有管理员权限) + * @date 2021-03-10 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RolePermission { + +} diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/PermissionAspect.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/PermissionAspect.java new file mode 100644 index 0000000..cc755de --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/PermissionAspect.java @@ -0,0 +1,122 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.permission.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.dubhe.biz.base.context.DataContext; +import org.dubhe.biz.permission.annotation.DataPermissionMethod; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.CommonPermissionDataDTO; +import org.dubhe.biz.base.enums.DatasetTypeEnum; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * @description 数据权限切面 + * @date 2020-11-26 + */ +@Aspect +@Component +public class PermissionAspect { + + @Resource + private UserContextService userContextService; + + /** + * 公共数据的有用户ID + */ + public static final Long PUBLIC_DATA_USER_ID = 0L; + + /** + * 基于注解的切面方法 + */ + @Pointcut("@annotation(org.dubhe.biz.permission.annotation.DataPermissionMethod)") + private void cutMethod() { + + } + + /** + *环绕通知 + * @param joinPoint 切入参数对象 + * @return 返回方法结果集 + * @throws Throwable + */ + @Around("cutMethod()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + // 获取方法传入参数 + Object[] params = joinPoint.getArgs(); + DataPermissionMethod dataPermissionMethod = getDeclaredAnnotation(joinPoint); + UserContext curUser = userContextService.getCurUser(); + + if (!Objects.isNull(curUser) && !Objects.isNull(dataPermissionMethod)) { + Set ids = new HashSet<>(); + ids.add(curUser.getId()); + CommonPermissionDataDTO commonPermissionDataDTO = CommonPermissionDataDTO.builder().type(dataPermissionMethod.interceptFlag()).resourceUserIds(ids).build(); + if (DatasetTypeEnum.PUBLIC.equals(dataPermissionMethod.dataType())) { + ids.add(PUBLIC_DATA_USER_ID); + commonPermissionDataDTO.setResourceUserIds(ids); + } + DataContext.set(commonPermissionDataDTO); + } + // 执行源方法 + try { + return joinPoint.proceed(params); + } finally { + if(!Objects.isNull(DataContext.get())){ + DataContext.remove(); + } + } + } + + /** + * 获取方法中声明的注解 + * + * @param joinPoint 切入参数对象 + * @return DataPermissionMethod 方法注解类型 + */ + public DataPermissionMethod getDeclaredAnnotation(ProceedingJoinPoint joinPoint){ + // 获取方法名 + String methodName = joinPoint.getSignature().getName(); + // 反射获取目标类 + Class targetClass = joinPoint.getTarget().getClass(); + // 拿到方法对应的参数类型 + Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes(); + // 根据类、方法、参数类型(重载)获取到方法的具体信息 + Method objMethod = null; + try { + objMethod = targetClass.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + LogUtil.error(LogEnum.BIZ_DATASET,"获取注解方法参数异常 error:{}",e); + } + // 拿到方法定义的注解信息 + DataPermissionMethod annotation = objMethod.getDeclaredAnnotation(DataPermissionMethod.class); + // 返回 + return annotation; + } +} diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/RolePermissionAspect.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/RolePermissionAspect.java new file mode 100644 index 0000000..61bff69 --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/aspect/RolePermissionAspect.java @@ -0,0 +1,57 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.permission.aspect; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.dubhe.biz.base.enums.BaseErrorCodeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.permission.base.BaseService; +import org.springframework.stereotype.Component; + +/** + * @description 角色权限切面(验证是否具有管理员权限) + * @date 2020-11-26 + */ +@Aspect +@Component +public class RolePermissionAspect { + + + /** + * 基于注解的切面方法 + */ + @Pointcut("@annotation(org.dubhe.biz.permission.annotation.RolePermission)") + public void cutMethod() { + + } + /** + * 前置通知 验证是否具有管理员权限 + * + * @param point 切入参数对象 + * @return 返回方法结果集 + */ + @Before(value = "cutMethod()") + public void before(JoinPoint point) { + if (!BaseService.isAdmin()) { + throw new BusinessException(BaseErrorCodeEnum.DATASET_ADMIN_PERMISSION_ERROR); + } + } + +} diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/base/BaseService.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/base/BaseService.java new file mode 100644 index 0000000..a21192a --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/base/BaseService.java @@ -0,0 +1,81 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.permission.base; + +import org.dubhe.biz.base.context.DataContext; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.enums.BaseErrorCodeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.permission.util.SqlUtil; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @description 服务层基础数据公共方法类 + * @date 2020-03-27 + */ +public class BaseService { + + private BaseService(){} + + /** + * 校验是否具有管理员权限 + */ + public static void checkAdminPermission() { + UserContextService userContextService = SpringContextHolder.getBean(UserContextService.class); + if(!isAdmin(userContextService.getCurUser())){ + throw new BusinessException(BaseErrorCodeEnum.DATASET_ADMIN_PERMISSION_ERROR); + } + } + + /** + * 校验是否具有管理员权限 + */ + public static Boolean isAdmin() { + UserContextService userContextService = SpringContextHolder.getBean(UserContextService.class); + return isAdmin(userContextService.getCurUser()); + } + + /** + * 校验是否是管理管理员 + * + * @return 校验标识 + */ + public static Boolean isAdmin(UserContext userContext) { + if (!CollectionUtils.isEmpty(userContext.getRoles())) { + List roleIds = userContext.getRoles().stream().map(a -> a.getId()).collect(Collectors.toList()); + return SqlUtil.isAdmin(roleIds); + } + return false; + } + + + /** + * 清除本地线程数据权限数据 + */ + public static void removeContext(){ + if( !Objects.isNull(DataContext.get())){ + DataContext.remove(); + } + } + +} diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MetaHandlerConfig.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MetaHandlerConfig.java new file mode 100644 index 0000000..f5aaf6d --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MetaHandlerConfig.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.permission.config; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.DateUtil; +import org.dubhe.biz.db.constant.MetaHandlerConstant; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Objects; + +/** + * @description 处理新增和更新的基础数据填充,配合BaseEntity和MyBatisPlusConfig使用 + * @date 2020-11-26 + */ +@Component +public class MetaHandlerConfig implements MetaObjectHandler { + + + @Resource + private UserContextService userContextService; + + /** + * 新增数据执行 + * + * @param metaObject 基础数据 + */ + @Override + public void insertFill(MetaObject metaObject) { + if (Objects.isNull(getFieldValByName(MetaHandlerConstant.CREATE_TIME, metaObject))) { + this.setFieldValByName(MetaHandlerConstant.CREATE_TIME, DateUtil.getCurrentTimestamp(), metaObject); + } + if (Objects.isNull(getFieldValByName(MetaHandlerConstant.UPDATE_TIME, metaObject))) { + this.setFieldValByName(MetaHandlerConstant.UPDATE_TIME, DateUtil.getCurrentTimestamp(), metaObject); + } + if (Objects.isNull(getFieldValByName(MetaHandlerConstant.UPDATE_USER_ID, metaObject))) { + this.setFieldValByName(MetaHandlerConstant.UPDATE_USER_ID, userContextService.getCurUserId(), metaObject); + } + if (Objects.isNull(getFieldValByName(MetaHandlerConstant.CREATE_USER_ID, metaObject))) { + this.setFieldValByName(MetaHandlerConstant.CREATE_USER_ID, userContextService.getCurUserId(), metaObject); + } + if (Objects.isNull(getFieldValByName(MetaHandlerConstant.ORIGIN_USER_ID, metaObject))) { + this.setFieldValByName(MetaHandlerConstant.ORIGIN_USER_ID, userContextService.getCurUserId(), metaObject); + } + if (Objects.isNull(getFieldValByName(MetaHandlerConstant.DELETED, metaObject))) { + this.setFieldValByName(MetaHandlerConstant.DELETED, false, metaObject); + } + } + + /** + * 更新数据执行 + * + * @param metaObject 基础数据 + */ + @Override + public void updateFill(MetaObject metaObject) { + this.setFieldValByName(MetaHandlerConstant.UPDATE_TIME, DateUtil.getCurrentTimestamp(), metaObject); + this.setFieldValByName(MetaHandlerConstant.UPDATE_USER_ID, userContextService.getCurUserId(), metaObject); + } + + +} diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MybatisPlusConfig.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MybatisPlusConfig.java new file mode 100644 index 0000000..52499db --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/config/MybatisPlusConfig.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.permission.config; + +import org.dubhe.biz.permission.interceptor.PaginationInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @description mybatis plus拦截器 + * @date 2020-11-26 + */ +@EnableTransactionManagement +@Configuration +public class MybatisPlusConfig { + + /** + * 注入 MybatisPlus 分页拦截器 + * + * @return 自定义MybatisPlus分页拦截器 + */ + @Bean + public PaginationInterceptor paginationInterceptor() { + PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); + return paginationInterceptor; + } +} + diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/CustomerSqlInterceptor.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/CustomerSqlInterceptor.java new file mode 100644 index 0000000..967796b --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/CustomerSqlInterceptor.java @@ -0,0 +1,117 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.permission.interceptor; + +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.DefaultReflectorFactory; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.dubhe.biz.base.annotation.DataPermission; +import org.dubhe.biz.base.context.DataContext; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.enums.OperationTypeEnum; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.permission.util.SqlUtil; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.lang.reflect.Field; +import java.sql.Connection; +import java.util.Arrays; +import java.util.Objects; +import java.util.Properties; + + +/** + * @description mybatis拦截器 + * @date 2020-11-25 + */ +@Component +@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) +public class CustomerSqlInterceptor implements Interceptor { + + @Resource + private UserContextService userContextService; + + @Override + public Object intercept(Invocation invocation) throws Throwable { + + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, + SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory()); + /* + * 先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler, + * 然后就到BaseStatementHandler的成员变量mappedStatement + */ + MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); + //id为执行的mapper方法的全路径名,如com.uv.dao.UserDao.selectPageVo + String id = mappedStatement.getId(); + //sql语句类型 select、delete、insert、update + String sqlCommandType = mappedStatement.getSqlCommandType().toString(); + BoundSql boundSql = statementHandler.getBoundSql(); + + //获取到原始sql语句 + String sql = boundSql.getSql(); + String mSql = sql; + + //注解逻辑判断 添加注解了才拦截 + Class classType = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf("."))); + String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length()); + + //获取类注解 获取需要忽略拦截的方法名称 + DataPermission dataAnnotation = classType.getAnnotation(DataPermission.class); + if (!Objects.isNull(dataAnnotation)) { + UserContext curUser = userContextService.getCurUser(); + String[] ignores = dataAnnotation.ignoresMethod(); + //校验拦截忽略方法名 忽略新增方法 忽略回调/定时方法 + if ((!Objects.isNull(ignores) && Arrays.asList(ignores).contains(mName)) + || OperationTypeEnum.INSERT.getType().equals(sqlCommandType.toLowerCase()) + || Objects.isNull(curUser) + || (!Objects.isNull(DataContext.get()) && DataContext.get().getType()) + ) { + return invocation.proceed(); + } else { + //拦截所有sql操作类型 + mSql = SqlUtil.buildTargetSql(sql, SqlUtil.getResourceIds(curUser),curUser); + } + } + + //通过反射修改sql语句 + Field field = boundSql.getClass().getDeclaredField("sql"); + field.setAccessible(true); + field.set(boundSql, mSql); + return invocation.proceed(); + } + + @Override + public Object plugin(Object target) { + if (target instanceof StatementHandler) { + return Plugin.wrap(target, this); + } else { + return target; + } + } + + @Override + public void setProperties(Properties properties) { + + } + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/PaginationInterceptor.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/PaginationInterceptor.java new file mode 100644 index 0000000..f785952 --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/interceptor/PaginationInterceptor.java @@ -0,0 +1,463 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.permission.interceptor; + + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.MybatisDefaultParameterHandler; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.core.parser.ISqlParser; +import com.baomidou.mybatisplus.core.parser.SqlInfo; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler; +import com.baomidou.mybatisplus.extension.plugins.pagination.DialectFactory; +import com.baomidou.mybatisplus.extension.plugins.pagination.DialectModel; +import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect; +import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils; +import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.*; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.mapping.*; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; +import org.apache.ibatis.session.Configuration; +import org.dubhe.biz.base.annotation.DataPermission; +import org.dubhe.biz.base.context.DataContext; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.enums.OperationTypeEnum; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.permission.util.SqlUtil; + +import javax.annotation.Resource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description MybatisPlus 分页拦截器 + * @date 2020-11-25 + */ +@Intercepts({@Signature( + type = StatementHandler.class, + method = "prepare", + args = {Connection.class, Integer.class} +)}) +public class PaginationInterceptor extends AbstractSqlParserHandler implements Interceptor { + protected static final Log logger = LogFactory.getLog(PaginationInterceptor.class); + + + @Resource + private UserContextService userContextService; + + /** + * COUNT SQL 解析 + */ + protected ISqlParser countSqlParser; + /** + * 溢出总页数,设置第一页 + */ + protected boolean overflow = false; + /** + * 单页限制 500 条,小于 0 如 -1 不受限制 + */ + protected long limit = 500L; + /** + * 数据类型 + */ + private DbType dbType; + /** + * 方言 + */ + private IDialect dialect; + /** + * 方言类型 + */ + @Deprecated + protected String dialectType; + /** + * 方言实现类 + */ + @Deprecated + protected String dialectClazz; + + public PaginationInterceptor() { + } + + /** + * 构建分页sql + * + * @param originalSql 原生sql + * @param page 分页参数 + * @return 构建后 sql + */ + public static String concatOrderBy(String originalSql, IPage page) { + if (CollectionUtils.isNotEmpty(page.orders())) { + try { + List orderList = page.orders(); + Select selectStatement = (Select) CCJSqlParserUtil.parse(originalSql); + List orderByElements; + List orderByElementsReturn; + if (selectStatement.getSelectBody() instanceof PlainSelect) { + PlainSelect plainSelect = (PlainSelect) selectStatement.getSelectBody(); + orderByElements = plainSelect.getOrderByElements(); + orderByElementsReturn = addOrderByElements(orderList, orderByElements); + plainSelect.setOrderByElements(orderByElementsReturn); + return plainSelect.toString(); + } + + if (selectStatement.getSelectBody() instanceof SetOperationList) { + SetOperationList setOperationList = (SetOperationList) selectStatement.getSelectBody(); + orderByElements = setOperationList.getOrderByElements(); + orderByElementsReturn = addOrderByElements(orderList, orderByElements); + setOperationList.setOrderByElements(orderByElementsReturn); + return setOperationList.toString(); + } + + if (selectStatement.getSelectBody() instanceof WithItem) { + return originalSql; + } + + return originalSql; + } catch (JSQLParserException var7) { + logger.error("failed to concat orderBy from IPage, exception=", var7); + } + } + + return originalSql; + } + + /** + * 添加分页排序规则 + * + * @param orderList 分页规则 + * @param orderByElements 分页排序元素 + * @return 分页规则 + */ + private static List addOrderByElements(List orderList, List orderByElements) { + orderByElements = CollectionUtils.isEmpty(orderByElements) ? new ArrayList(orderList.size()) : orderByElements; + List orderByElementList = orderList.stream().filter((item) -> { + return StringUtils.isNotBlank(item.getColumn()); + }).map((item) -> { + OrderByElement element = new OrderByElement(); + element.setExpression(new Column(item.getColumn())); + element.setAsc(item.isAsc()); + element.setAscDescPresent(true); + return element; + }).collect(Collectors.toList()); + ( orderByElements).addAll(orderByElementList); + return orderByElements; + } + + /** + * 执行sql查询逻辑 + * + * @param invocation mybatis 调用类 + * @return + * @throws Throwable + */ + @Override + public Object intercept(Invocation invocation) throws Throwable { + StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); + MetaObject metaObject = SystemMetaObject.forObject(statementHandler); + this.sqlParser(metaObject); + MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); + if (SqlCommandType.SELECT == mappedStatement.getSqlCommandType() && StatementType.CALLABLE != mappedStatement.getStatementType()) { + BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); + Object paramObj = boundSql.getParameterObject(); + IPage page = null; + if (paramObj instanceof IPage) { + page = (IPage) paramObj; + } else if (paramObj instanceof Map) { + Iterator var8 = ((Map) paramObj).values().iterator(); + + while (var8.hasNext()) { + Object arg = var8.next(); + if (arg instanceof IPage) { + page = (IPage) arg; + break; + } + } + } + + if (null != page && page.getSize() >= 0L) { + if (this.limit > 0L && this.limit <= page.getSize()) { + this.handlerLimit(page); + } + + String originalSql = boundSql.getSql(); + + //注解逻辑判断 添加注解了才拦截 + Class classType = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf("."))); + String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length()); + + + String sqlCommandType = mappedStatement.getSqlCommandType().toString(); + //获取类注解 获取需要忽略拦截的方法名称 + DataPermission dataAnnotation = classType.getAnnotation(DataPermission.class); + if (!Objects.isNull(dataAnnotation)) { + UserContext curUser = userContextService.getCurUser(); + String[] ignores = dataAnnotation.ignoresMethod(); + //校验拦截忽略方法名 忽略新增方法 忽略回调/定时方法 + if (!((!Objects.isNull(ignores) && Arrays.asList(ignores).contains(mName)) + || OperationTypeEnum.INSERT.getType().equals(sqlCommandType.toLowerCase()) + || Objects.isNull(curUser) + || (!Objects.isNull(DataContext.get()) && DataContext.get().getType())) + + + ) { + originalSql = SqlUtil.buildTargetSql(originalSql, SqlUtil.getResourceIds(curUser), curUser); + } + } + + Connection connection = (Connection) invocation.getArgs()[0]; + if (page.isSearchCount() && !page.isHitCount()) { + SqlInfo sqlInfo = SqlParserUtils.getOptimizeCountSql(page.optimizeCountSql(), this.countSqlParser, originalSql); + this.queryTotal(sqlInfo.getSql(), mappedStatement, boundSql, page, connection); + if (page.getTotal() <= 0L) { + return null; + } + } + + DbType dbType = Optional.ofNullable(this.dbType).orElse(JdbcUtils.getDbType(connection.getMetaData().getURL())); + IDialect dialect = Optional.ofNullable(this.dialect).orElse(DialectFactory.getDialect(dbType)); + String buildSql = concatOrderBy(originalSql, page); + DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize()); + Configuration configuration = mappedStatement.getConfiguration(); + List mappings = new ArrayList(boundSql.getParameterMappings()); + Map additionalParameters = (Map) metaObject.getValue("delegate.boundSql.additionalParameters"); + model.consumers(mappings, configuration, additionalParameters); + metaObject.setValue("delegate.boundSql.sql", model.getDialectSql()); + metaObject.setValue("delegate.boundSql.parameterMappings", mappings); + return invocation.proceed(); + } else { + return invocation.proceed(); + } + } else { + return invocation.proceed(); + } + } + + /** + * 处理分页数量 + * + * @param page 分页参数 + */ + protected void handlerLimit(IPage page) { + page.setSize(this.limit); + } + + /** + * 查询总数量 + * + * @param sql sql语句 + * @param mappedStatement 映射语句包装类 + * @param boundSql sql包装类 + * @param page 分页参数 + * @param connection JDBC连接包装类 + */ + protected void queryTotal(String sql, MappedStatement mappedStatement, BoundSql boundSql, IPage page, Connection connection) { + try { + PreparedStatement statement = connection.prepareStatement(sql); + Throwable var7 = null; + + try { + DefaultParameterHandler parameterHandler = new MybatisDefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql); + parameterHandler.setParameters(statement); + long total = 0L; + ResultSet resultSet = statement.executeQuery(); + Throwable var12 = null; + + try { + if (resultSet.next()) { + total = resultSet.getLong(1); + } + } catch (Throwable var37) { + var12 = var37; + throw var37; + } finally { + if (resultSet != null) { + if (var12 != null) { + try { + resultSet.close(); + } catch (Throwable var36) { + var12.addSuppressed(var36); + } + } else { + resultSet.close(); + } + } + + } + + page.setTotal(total); + if (this.overflow && page.getCurrent() > page.getPages()) { + this.handlerOverflow(page); + } + } catch (Throwable var39) { + var7 = var39; + throw var39; + } finally { + if (statement != null) { + if (var7 != null) { + try { + statement.close(); + } catch (Throwable var35) { + var7.addSuppressed(var35); + } + } else { + statement.close(); + } + } + + } + + } catch (Exception var41) { + throw ExceptionUtils.mpe("Error: Method queryTotal execution error of sql : \n %s \n", var41, new Object[]{sql}); + } + } + + /** + * 设置默认当前页 + * + * @param page 分页参数 + */ + protected void handlerOverflow(IPage page) { + page.setCurrent(1L); + } + + /** + * MybatisPlus拦截器实现自定义插件 + * + * @param target 拦截目标对象 + * @return + */ + @Override + public Object plugin(Object target) { + return target instanceof StatementHandler ? Plugin.wrap(target, this) : target; + } + + /** + * MybatisPlus拦截器实现自定义属性设置 + * + * @param prop 属性参数 + */ + @Override + public void setProperties(Properties prop) { + String dialectType = prop.getProperty("dialectType"); + String dialectClazz = prop.getProperty("dialectClazz"); + if (StringUtils.isNotBlank(dialectType)) { + this.setDialectType(dialectType); + } + + if (StringUtils.isNotBlank(dialectClazz)) { + this.setDialectClazz(dialectClazz); + } + + } + + /** + * 设置数据源类型 + * + * @param dialectType 数据源类型 + */ + @Deprecated + public void setDialectType(String dialectType) { + this.setDbType(DbType.getDbType(dialectType)); + } + + + /** + * 设置方言实现类配置 + * + * @param dialectClazz 方言实现类 + */ + @Deprecated + public void setDialectClazz(String dialectClazz) { + this.setDialect(DialectFactory.getDialect(dialectClazz)); + } + + /** + * 设置获取总数的sql解析器 + * + * @param countSqlParser 总数的sql解析器 + * @return 自定义MybatisPlus拦截器 + */ + public PaginationInterceptor setCountSqlParser(final ISqlParser countSqlParser) { + this.countSqlParser = countSqlParser; + return this; + } + + /** + * 溢出总页数,设置第一页 + * + * @param overflow 溢出总页数 + * @return 自定义MybatisPlus拦截器 + */ + public PaginationInterceptor setOverflow(final boolean overflow) { + this.overflow = overflow; + return this; + } + + /** + * 设置分页规则 + * + * @param limit 分页数量 + * @return 自定义MybatisPlus拦截器 + */ + public PaginationInterceptor setLimit(final long limit) { + this.limit = limit; + return this; + } + + /** + * 设置数据类型 + * + * @param dbType 数据类型 + * @return 自定义MybatisPlus拦截器 + */ + public PaginationInterceptor setDbType(final DbType dbType) { + this.dbType = dbType; + return this; + } + + /** + * 设置方言 + * + * @param dialect 方言 + * @return 自定义MybatisPlus拦截器 + */ + public PaginationInterceptor setDialect(final IDialect dialect) { + this.dialect = dialect; + return this; + } + + +} + diff --git a/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/util/SqlUtil.java b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/util/SqlUtil.java new file mode 100644 index 0000000..cd14bdb --- /dev/null +++ b/dubhe-server/common-biz/data-permission/src/main/java/org/dubhe/biz/permission/util/SqlUtil.java @@ -0,0 +1,114 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.permission.util; + +import org.apache.commons.lang3.StringUtils; +import org.dubhe.biz.base.context.DataContext; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.enums.BaseErrorCodeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.db.constant.PermissionConstant; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description sql语句转换的工具类 + * @date 2020-11-25 + */ +public class SqlUtil { + + + /** + * 获取资源拥有着ID + * + * @return 资源拥有者id集合 + */ + public static Set getResourceIds(UserContext curUser) { + if (!Objects.isNull(DataContext.get())) { + return DataContext.get().getResourceUserIds(); + } + Set ids = new HashSet<>(); + ids.add(curUser.getId()); + return ids; + + } + + + /** + * 构建目标sql语句 + * + * @param originSql 原生sql + * @param resourceUserIds 所属资源用户ids + * @return 目标sql + */ + public static String buildTargetSql(String originSql, Set resourceUserIds, UserContext userContext) { + if(Objects.isNull(userContext)){ + throw new BusinessException(BaseErrorCodeEnum.SYSTEM_USER_IS_NOT_EXISTS.getCode(), + BaseErrorCodeEnum.SYSTEM_USER_IS_NOT_EXISTS.getMsg()); + + } + if (isAdmin(userContext.getRoles().stream().map(a->a.getId()).collect(Collectors.toList()))) { + return originSql; + } + String sqlWhereBefore = StringUtils.substringBefore(originSql.toLowerCase(), "where"); + String sqlWhereAfter = StringUtils.substringAfter(originSql.toLowerCase(), "where"); + StringBuffer buffer = new StringBuffer(); + //操作的sql拼接 + String targetSql = buffer.append(sqlWhereBefore).append(" where ").append(" origin_user_id in (") + .append(StringUtils.join(resourceUserIds, ",")).append(") and ").append(sqlWhereAfter).toString(); + + return targetSql; + } + + + /** + * 校验是否是管理管理员 (待权限添加获取角色再修改 默认都是管理员角色访问) + * + * @return 校验标识 + */ + public static Boolean isAdmin(List roleIds) { + return !CollectionUtils.isEmpty(roleIds) && roleIds.contains(PermissionConstant.ADMIN_ROLE_ID) ? true: false; + } + /** + * 将数组转换成in('1','2')的形式 + * + * @param objs + * @return + */ + public static String integerlistToString(Integer[] objs) { + + if (objs != null && objs.length > 0) { + StringBuilder sb = new StringBuilder("("); + for (Object obj : objs) { + if (obj != null) { + sb.append("'" + obj.toString() + "',"); + } + } + sb.deleteCharAt(sb.length() - 1); + sb.append(")"); + return sb.toString(); + } + return ""; + } + + + + +} diff --git a/dubhe-server/common-biz/data-response/pom.xml b/dubhe-server/common-biz/data-response/pom.xml new file mode 100644 index 0000000..7687f91 --- /dev/null +++ b/dubhe-server/common-biz/data-response/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.dubhe.biz + common-biz + 0.0.1-SNAPSHOT + + data-response + 0.0.1-SNAPSHOT + Biz 统一Rest返回工具结构 + Data response for Dubhe Server + + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + + org.springframework + spring-web + + + + org.springframework.security + spring-security-core + + + + + + + + + + + diff --git a/dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/exception/handler/GlobalExceptionHandler.java b/dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..744bbbc --- /dev/null +++ b/dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,155 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.dataresponse.exception.handler; + +import lombok.extern.slf4j.Slf4j; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.enums.BaseErrorCodeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.exception.CaptchaException; +import org.dubhe.biz.base.exception.FeignException; +import org.dubhe.biz.base.exception.OAuthResponseError; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Objects; + + +/** + * @description 全局异常处理器 + * @date 2020-02-23 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + + /** + * 处理自定义异常 + */ + @ExceptionHandler(value = AccessDeniedException.class) + public ResponseEntity loginException(AccessDeniedException e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "未授权引起异常的堆栈信息:{}", e); + return buildResponseEntity(HttpStatus.UNAUTHORIZED, + DataResponseFactory.failed(BaseErrorCodeEnum.UNAUTHORIZED.getCode(),BaseErrorCodeEnum.UNAUTHORIZED.getMsg())); + } + + + /** + * 处理自定义异常 + */ + @ExceptionHandler(value = IllegalStateException.class) + public ResponseEntity illegalStateException(IllegalStateException e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "服务未发现引起异常的堆栈信息:{}", e); + return buildResponseEntity(HttpStatus.NOT_FOUND, DataResponseFactory.failed(e.getMessage())); + } + + /** + * 处理OAuth2 授权异常 + */ + @ExceptionHandler(value = OAuthResponseError.class) + public ResponseEntity oAuth2ResponseError(OAuthResponseError e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "OAuth2 授权异常引发的异常的堆栈信息:{}", e); + return buildResponseEntity(e.getStatusCode(), e.getResponseBody()); + } + + + /** + * 处理所有不可知的异常 + */ + @ExceptionHandler(Throwable.class) + public ResponseEntity handleException(Throwable e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "引发的异常的堆栈信息:{}", e); + return buildResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, + DataResponseFactory.failed(e.getMessage())); + } + + + /** + * 处理自定义异常 + */ + @ExceptionHandler(value = BusinessException.class) + public ResponseEntity badRequestException(BusinessException e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "引发的异常的堆栈信息:{}", e); + return buildResponseEntity(HttpStatus.OK, e.getResponseBody()); + } + + /** + * 处理所有接口数据验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "引起异常的堆栈信息:{}", e); + String[] str = Objects.requireNonNull(e.getBindingResult().getAllErrors().get(0).getCodes())[1].split("\\."); + String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + String msg = "不能为空"; + if (msg.equals(message)) { + message = str[1] + ":" + message; + } + return buildResponseEntity(HttpStatus.BAD_REQUEST, new DataResponseBody(ResponseCode.ERROR, message)); + } + + /** + * 远程调用异常 + */ + @ExceptionHandler(FeignException.class) + public ResponseEntity feignException(FeignException e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "引发的异常的堆栈信息:{}", e); + return buildResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, + DataResponseFactory.failed(e.getResponseBody().getMsg())); + } + + + /** + * 验证码异常 + */ + @ExceptionHandler(CaptchaException.class) + public ResponseEntity captchaException(CaptchaException e) { + // 打印堆栈信息 + LogUtil.error(LogEnum.SYS_ERR, "引发的异常的堆栈信息:{}", e); + return buildResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, + DataResponseFactory.failed(e.getResponseBody().getMsg())); + } + + /** + * 统一返回 + * + * @param httpStatus + * @param responseBody + * @return + */ + private ResponseEntity buildResponseEntity(HttpStatus httpStatus, DataResponseBody responseBody) { + return new ResponseEntity<>(responseBody, httpStatus); + } + +} diff --git a/dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/factory/DataResponseFactory.java b/dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/factory/DataResponseFactory.java new file mode 100644 index 0000000..47c6c23 --- /dev/null +++ b/dubhe-server/common-biz/data-response/src/main/java/org/dubhe/biz/dataresponse/factory/DataResponseFactory.java @@ -0,0 +1,123 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.dataresponse.factory; + + +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.base.constant.ResponseCode; + +/** + * @description DataResponseBody 工厂类 + * @date 2020-05-28 + */ +public class DataResponseFactory { + + private DataResponseFactory(){ + + } + + /** + * 成功响应 + * + * @param + * @return + */ + public static DataResponseBody success(){ + return success(null,null); + } + + /** + * 成功响应 + * + * @param data + * @param + * @return + */ + public static DataResponseBody success(T data){ + return success(null,data); + } + + /** + * 成功响应 + * + * @param msg + * @return + */ + public static DataResponseBody successWithMsg(String msg){ + return success(msg,null); + } + + /** + * 成功响应 + * + * @param msg + * @param data + * @param + * @return + */ + public static DataResponseBody success(String msg, T data){ + return new DataResponseBody(ResponseCode.SUCCESS,msg,data); + } + + /** + * 失败响应 msg + * + * @param msg + * @return + */ + public static DataResponseBody failed(String msg){ + return failed(ResponseCode.ERROR,msg,null); + } + + /** + * 失败响应 + * + * @param failedCode + * @param msg + * @return + */ + public static DataResponseBody failed(Integer failedCode, String msg){ + return failed(failedCode,msg,null); + } + + /** + * 失败响应 + * + * @param failedCode + * @return + */ + public static DataResponseBody failed(Integer failedCode){ + return failed(failedCode,null,null); + } + + /** + * 失败响应 + * + * @param failedCode + * @param msg + * @param data + * @param + * @return + */ + public static DataResponseBody failed(Integer failedCode, String msg, T data){ + return new DataResponseBody(failedCode,msg,data); + } + + + +} diff --git a/dubhe-server/common-biz/db/pom.xml b/dubhe-server/common-biz/db/pom.xml new file mode 100644 index 0000000..34c09a9 --- /dev/null +++ b/dubhe-server/common-biz/db/pom.xml @@ -0,0 +1,62 @@ + + + + common-biz + org.dubhe.biz + 0.0.1-SNAPSHOT + + 4.0.0 + Biz 持久层操作 + db + + + + + mysql + mysql-connector-java + ${mysql.version} + runtime + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus + ${mybatis-plus.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + jakarta.validation + jakarta.validation-api + + + org.dubhe.biz + base + 0.0.1-SNAPSHOT + compile + + + io.swagger + swagger-annotations + 1.5.20 + compile + + + + \ No newline at end of file diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/annotation/Query.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/annotation/Query.java new file mode 100644 index 0000000..409bdda --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/annotation/Query.java @@ -0,0 +1,68 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.biz.db.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @description 构建Wrapper的注解 + * @date 2020-03-26 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Query { + + + String propName() default ""; + + Type type() default Type.EQ; + + + String blurry() default ""; + + enum Type { + // 相等 + EQ + // 不等于 + , NE + // 大于 + , GT + // 大于等于 + , GE + // 小于 + , LT + // 小于等于 + , LE, + BETWEEN, + NOT_BETWEEN, + LIKE, + NOT_LIKE, + LIkE_LEFT, + LIKE_RIGHT, + IS_NULL, + IS_NOT_NULL, + IN, + NOT_IN, + INSQL, + NOT_INSQL, + ORDER_BY + } + +} + diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/BaseConvert.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/BaseConvert.java new file mode 100644 index 0000000..608267a --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/BaseConvert.java @@ -0,0 +1,58 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.biz.db.base; + +import java.util.List; + +/** + * @description DTO Entity 转换 + * @date 2020-03-15 + */ +public interface BaseConvert { + + /** + * DTO转Entity + * + * @param dto / + * @return / + */ + E toEntity(D dto); + + /** + * Entity转DTO + * + * @param entity / + * @return / + */ + D toDto(E entity); + + /** + * DTO集合转Entity集合 + * + * @param dtoList / + * @return / + */ + List toEntity(List dtoList); + + /** + * Entity集合转DTO集合 + * + * @param entityList / + * @return / + */ + List toDto(List entityList); + +} diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/PageQueryBase.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/PageQueryBase.java new file mode 100644 index 0000000..e1208b3 --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/base/PageQueryBase.java @@ -0,0 +1,63 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.db.base; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description 分页基类 + * @date 2020-05-08 + */ +@Data +@Accessors(chain = true) +public class PageQueryBase { + + /** + * 分页-当前页数 + */ + private Integer current; + + /** + * 分页-每页展示数 + */ + private Integer size; + + /** + * 排序字段 + */ + private String sort; + + /** + * 排序方式,asc | desc + */ + private String order; + + public Page toPage() { + Page page = new Page(); + if (this.current != null) { + page.setCurrent(this.current); + } + if (this.size != null && this.size < 2000) { + page.setSize(this.size); + } + return page; + } + +} diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/MetaHandlerConstant.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/MetaHandlerConstant.java new file mode 100644 index 0000000..39f50d3 --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/MetaHandlerConstant.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.db.constant; + +/** + * @description 元数据枚举 + * @date 2020-11-26 + */ +public final class MetaHandlerConstant { + + /** + * 创建时间字段 + */ + public static final String CREATE_TIME = "createTime"; + /** + *更新时间字段 + */ + public static final String UPDATE_TIME = "updateTime"; + /** + *更新人id字段 + */ + public static final String UPDATE_USER_ID = "updateUserId"; + /** + *创建人id字段 + */ + public static final String CREATE_USER_ID = "createUserId"; + /** + *资源拥有人id字段 + */ + public static final String ORIGIN_USER_ID = "originUserId"; + /** + *删除字段 + */ + public static final String DELETED = "deleted"; + + private MetaHandlerConstant() { + } +} diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/PermissionConstant.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/PermissionConstant.java new file mode 100644 index 0000000..13eca95 --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/constant/PermissionConstant.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.db.constant; + +import lombok.Data; +import org.springframework.stereotype.Component; + +/** + * @description 权限常量 + * @date 2020-05-25 + */ +@Component +@Data +public class PermissionConstant { + + /** + * 超级用户ID + */ + public static final long ADMIN_USER_ID = 1L; + + /** + * 超级用户角色ID + */ + public static final long ADMIN_ROLE_ID = 1L; + +} diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/entity/BaseEntity.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/entity/BaseEntity.java new file mode 100644 index 0000000..ae3aca6 --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/entity/BaseEntity.java @@ -0,0 +1,76 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.db.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.sql.Timestamp; + +/** + * @description Entity基础类 + * @date 2020-11-26 + */ +@Data +public class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 删除标识 + **/ + @TableField(value = "deleted",fill = FieldFill.INSERT) + @TableLogic + private Boolean deleted = false; + + @TableField(value = "create_user_id",fill = FieldFill.INSERT) + private Long createUserId; + + @TableField(value = "update_user_id",fill = FieldFill.INSERT_UPDATE) + private Long updateUserId; + + @TableField(value = "create_time",fill = FieldFill.INSERT) + private Timestamp createTime; + + + @TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE) + private Timestamp updateTime; + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + Field[] fields = this.getClass().getDeclaredFields(); + try { + for (Field f : fields) { + f.setAccessible(true); + builder.append(f.getName(), f.get(this)).append("\n"); + } + } catch (Exception e) { + builder.append("toString builder encounter an error"); + } + return builder.toString(); + } + + public @interface Update { + } +} diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/PageUtil.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/PageUtil.java new file mode 100644 index 0000000..c11cb04 --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/PageUtil.java @@ -0,0 +1,78 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.db.utils; + +import com.baomidou.mybatisplus.core.metadata.IPage; + +import java.util.*; +import java.util.function.Function; + +/** + * @description 分页工具 + * @date 2020-03-13 + */ +public class PageUtil extends cn.hutool.core.util.PageUtil { + + /** + * List 分页 + */ + public static List toPage(int page, int size, List list) { + int fromIndex = page * size; + int toIndex = page * size + size; + if (fromIndex > list.size()) { + return new ArrayList(); + } else if (toIndex >= list.size()) { + return list.subList(fromIndex, list.size()); + } else { + return list.subList(fromIndex, toIndex); + } + } + + /** + * Page 数据处理,预防redis反序列化报错 + */ + public static Map toPage(IPage page) { + return toPage(page, page.getRecords()); + } + + /** + * 自定义分页 + */ + public static Map toPage(IPage page, Function function) { + return toPage(page, function.apply(page.getRecords())); + } + + /** + * 自定义分页 + */ + public static Map toPage(IPage page, Collection data) { + Map map = new LinkedHashMap<>(2); + map.put("result", data); + map.put("page", buildPagination(page)); + return map; + } + + private static Map buildPagination(IPage page) { + Map map = new HashMap<>(2); + map.put("current", page.getCurrent()); + map.put("size", page.getSize()); + map.put("total", page.getTotal()); + return map; + } + +} diff --git a/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/WrapperHelp.java b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/WrapperHelp.java new file mode 100644 index 0000000..a3cb553 --- /dev/null +++ b/dubhe-server/common-biz/db/src/main/java/org/dubhe/biz/db/utils/WrapperHelp.java @@ -0,0 +1,162 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.db.utils; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.dubhe.biz.db.annotation.Query; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * @description 构建Wrapper + * @date 2020-03-15 + */ +@Slf4j +public class WrapperHelp { + + public static QueryWrapper getWrapper(T query) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (query == null) { + return queryWrapper; + } + try { + List fields = getAllFields(query.getClass(), new ArrayList<>()); + for (Field field : fields) { + field.setAccessible(true); + Query q = field.getAnnotation(Query.class); + if (q != null) { + String propName = q.propName(); + String attributeName = isBlank(propName) ? field.getName() : propName; + Object val = field.get(query); + if (val == null) { + continue; + } + // 模糊多字段 + String blurry = q.blurry(); + if (ObjectUtil.isNotEmpty(blurry)) { + String[] blurrys = blurry.split(","); + queryWrapper.and(qw -> { + for (int i = 0; i < blurrys.length; i++) { + if (i == 0) { + qw.like(blurrys[i], val); + } else { + qw.or().like(blurrys[i], val); + } + } + }); + continue; + } + switch (q.type()) { + case EQ: + queryWrapper = queryWrapper.eq(attributeName, val); + break; + case NE: + queryWrapper = queryWrapper.ne(attributeName, val); + break; + case GE: + queryWrapper = queryWrapper.ge(attributeName, val); + break; + case GT: + queryWrapper = queryWrapper.gt(attributeName, val); + break; + case LT: + queryWrapper = queryWrapper.lt(attributeName, val); + break; + case LE: + queryWrapper = queryWrapper.le(attributeName, val); + break; + case BETWEEN: + List between = new ArrayList<>((List) val); + queryWrapper = queryWrapper.between(attributeName, between.get(0), between.get(1)); + break; + case NOT_BETWEEN: + List notBetween = new ArrayList<>((List) val); + queryWrapper = queryWrapper.notBetween(attributeName, notBetween.get(0), notBetween.get(1)); + break; + case LIKE: + queryWrapper = queryWrapper.like(attributeName, val); + break; + case NOT_LIKE: + queryWrapper = queryWrapper.notLike(attributeName, val); + break; + case LIkE_LEFT: + queryWrapper = queryWrapper.likeLeft(attributeName, val); + break; + case LIKE_RIGHT: + queryWrapper = queryWrapper.likeRight(attributeName, val); + break; + case IS_NULL: + queryWrapper = queryWrapper.isNull(attributeName); + break; + case IS_NOT_NULL: + queryWrapper = queryWrapper.isNotNull(attributeName); + break; + case IN: + queryWrapper = queryWrapper.in(attributeName, (Collection) val); + break; + case NOT_IN: + queryWrapper = queryWrapper.notIn(attributeName, (Collection) val); + break; + case INSQL: + queryWrapper = queryWrapper.inSql(attributeName, String.valueOf(val)); + break; + case NOT_INSQL: + queryWrapper = queryWrapper.notInSql(attributeName, String.valueOf(val)); + break; + case ORDER_BY: + queryWrapper = queryWrapper.last(" ORDER BY " + val); + break; + default: + break; + } + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return queryWrapper; + } + + private static boolean isBlank(final CharSequence cs) { + int strLen; + if (cs == null || (strLen = cs.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + return true; + } + + private static List getAllFields(Class clazz, List fields) { + if (clazz != null) { + fields.addAll(Arrays.asList(clazz.getDeclaredFields())); + getAllFields(clazz.getSuperclass(), fields); + } + return fields; + } + +} diff --git a/dubhe-server/common-biz/file/pom.xml b/dubhe-server/common-biz/file/pom.xml new file mode 100644 index 0000000..4f411e3 --- /dev/null +++ b/dubhe-server/common-biz/file/pom.xml @@ -0,0 +1,82 @@ + + + + common-biz + org.dubhe.biz + 0.0.1-SNAPSHOT + + 4.0.0 + + file + 0.0.1-SNAPSHOT + Biz file 工具 + File util for Dubhe Server + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.springframework + spring-web + + + + + commons-io + commons-io + compile + + + + io.minio + minio + + + + org.apache.commons + commons-compress + + + + org.apache.commons + commons-pool2 + + + + com.emc.ecs + nfs-client + + + + org.apache.poi + poi + + + org.apache.poi + poi-ooxml + + + + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/FileStoreApi.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/FileStoreApi.java new file mode 100644 index 0000000..8a2f6e4 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/FileStoreApi.java @@ -0,0 +1,237 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.api; + +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.dto.FilePageDTO; + +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedInputStream; +import java.io.File; +import java.util.List; + +/** + * @description 文件存储接口 + * @date 2021-04-19 + */ +public interface FileStoreApi { + + /** + * 获取根路径 + * + * @return String 根路径 默认为空 + */ + default String getRootDir() { + return ""; + } + + /** + * 获取bucket + * + * @return String bucket 默认为空 + */ + default String getBucket() { + return ""; + } + + /** + * 替换路径中多余的 "/" + * + * @param path + * @return String + */ + default String formatPath(String path) { + if (!StringUtils.isEmpty(path)) { + return path.replaceAll("///*", File.separator); + } + return path; + } + + /** + * 绝对路径兼容 + * @param sourcePath 源路径 + * @return String + */ + default String compatibleAbsolutePath(String sourcePath) { + return formatPath(sourcePath.startsWith(getRootDir()) ? sourcePath : getRootDir() + sourcePath); + } + + + /** + * 校验文件或文件夹是否不存在(true为存在,false为不存在) + * + * @param path 文件路径 + * @return boolean + */ + boolean fileOrDirIsExist(String path); + + /** + * 校验是否是文件夹(true为文件夹,false为文件) + * + * @param path 文件路径 + * @return boolean + */ + boolean isDirectory(String path); + + /** + * 过滤路径下文件后缀名(不区分大小写)并返回符合条件的文件集合 + * + * @param path 文件夹路径 + * @param fileSuffix 文件后缀名(不区分大小写) + * @return List 返回符合条件的文件集合 + */ + List filterFileSuffix(String path,String fileSuffix); + + /** + * 创建指定目录 + * + * @param dir 需要创建的目录 例如:/abc/def + * @return boolean + */ + boolean createDir(String dir); + + /** + * 创建多个指定目录 + * + * @param paths + * @return boolean + */ + boolean createDirs(String... paths); + + /** + * 指定目录中创建文件 + * + * @param dir 需要创建的目录 例如:/abc/def + * @param fileName 需要创建的文件 例如:dd.txt + * @return boolean + */ + boolean createFile(String dir, String fileName); + + /** + * 新建或追加文件 + * + * @param filePath 文件绝对路径 + * @param content 文件内容 + * @param append 文件是否是追加 + * @return + */ + boolean createOrAppendFile(String filePath, String content, boolean append); + + /** + * 删除目录 或者文件 + * + * @param dirOrFile 需要删除的目录 或者文件 例如:/abc/def 或者 /abc/def/dd.txt + * @return boolean + */ + boolean deleteDirOrFile(String dirOrFile); + + /** + * 复制文件 到指定目录下 单个文件 + * + * @param sourceFile 需要复制的文件 例如:/abc/def/dd.txt + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + boolean copyFile(String sourceFile, String targetPath); + + /** + * 使用shell拷贝文件或路径 + * + * @param sourcePath 需要复制的文件或路径 例如:/abc/def/cc.txt or /abc/def* + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @param type CopyTypeEnum的 + * @return boolean + */ + boolean copyFile(String sourcePath, String targetPath, Integer type); + + /** + * 复制路径下文件并重命名 + * + * @param sourceFile 源文件 + * @param targetFile 目标文件全路径 + * @return boolean + */ + boolean copyFileAndRename(String sourceFile, String targetFile); + + /** + * 复制路径下文件及文件夹到指定目录下 多个文件 包含目录与文件并存情况 + * + * @param sourcePath 需要复制的文件目录 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + boolean copyPath(String sourcePath, String targetPath); + + /** + * 复制文件夹到指定目录下 + * + * @param sourcePath 需要复制的文件夹 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd 复制成功路径/abc/dd/def* + * @return boolean + */ + boolean copyDir(String sourcePath, String targetPath); + + /** + * zip解压并删除压缩文件 + * @param sourceFile zip源文件 例如:/abc/z.zip + * @param targetPath 解压后的目标文件夹 例如:/abc/ + * @return boolean + */ + boolean unzip(String sourceFile, String targetPath); + + /** + * 解压压缩包 包含目录与子目录 + * + * @param sourceFile 需要复制的文件 例如:/abc/def/aaa.zip + * @return boolean + */ + boolean unzip(String sourceFile); + + /** + * 压缩 目录 或者文件 到压缩包 + * + * @param dirOrFile 目录或者文件 例如: /abc/def/aaa.txt , /abc/def + * @param zipPath 压缩包全路径名 例如: /abc/def/aa.zip + * @return boolean + */ + boolean zipDirOrFile(String dirOrFile, String zipPath); + + /** + * 获取NFS3File 对象文件的输入流 + * + * @param path 文件路径 + * @return BufferedInputStream + */ + BufferedInputStream getInputStream(String path); + + /** + * 下载 + * + * @param path 文件路径 + * @param response HTTP返回 + */ + void download(String path, HttpServletResponse response); + + /** + * 分页查询指定路径下的文件列表(需要支持分页) + * + * @param filePageDTO 查询以及相应参数实体 + */ + void filterFilePageWithPath(FilePageDTO filePageDTO); + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/HostFileStoreApiImpl.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/HostFileStoreApiImpl.java new file mode 100644 index 0000000..2f46f67 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/HostFileStoreApiImpl.java @@ -0,0 +1,666 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.api.impl; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ZipUtil; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.poi.util.IOUtils; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.dto.FileDTO; +import org.dubhe.biz.file.dto.FilePageDTO; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.FileCopyUtils; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; + +/** + * @description 本地文件存储接口实现类 + * @date 2021-04-23 + */ +@Component(value = "hostFileStoreApiImpl") +public class HostFileStoreApiImpl implements FileStoreApi { + + private static final String FILE_SEPARATOR = File.separator; + + private static final String ZIP = ".zip"; + + private static final String CHARACTER_GBK = "GBK"; + + private static final String OS_NAME = "os.name"; + + private static final String WINDOWS = "Windows"; + + @Value("${storage.file-store-root-path}") + private String rootDir; + + @Value("/${minio.bucketName}/") + private String bucket; + + @Value("${storage.file-store-root-windows-path}") + private String rootWindowsPath; + + @Override + public String getRootDir() { + return rootDir; + } + + @Override + public String getBucket() { + return bucket; + } + + /** + * 校验文件或文件夹是否不存在(true为存在,false为不存在) + * + * @param path 文件路径 + * @return boolean + */ + @Override + public boolean fileOrDirIsExist(String path) { + path = compatibleAbsolutePath(path); + if (StringUtils.isBlank(path)) { + return false; + } + File file = new File(path); + return file.exists(); + } + + /** + * 校验是否是文件夹(true为文件夹,false为文件) + * + * @param path 文件路径 + * @return boolean + */ + @Override + public boolean isDirectory(String path) { + path = compatibleAbsolutePath(path); + if (StringUtils.isBlank(path)) { + return false; + } + File file = new File(path); + return file.isDirectory(); + } + + /** + * 过滤路径下文件后缀名(不区分大小写)并返回符合条件的文件集合 + * + * @param path 文件夹路径 + * @param fileSuffix 文件后缀名(不区分大小写) + * @return List 返回符合条件的文件集合 + */ + @Override + public List filterFileSuffix(String path, String fileSuffix) { + if (StringUtils.isEmpty(path) || StringUtils.isEmpty(fileSuffix)) { + return null; + } + path = compatibleAbsolutePath(path); + File sourceFile = new File(path); + List filePaths = new ArrayList<>(); + if (!sourceFile.exists()) { + return null; + } + File[] files = sourceFile.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (file.isFile()) { + if (file.getName().toLowerCase().endsWith(fileSuffix.toLowerCase())) { + filePaths.add(formatPath(path + FILE_SEPARATOR + file.getName())); + } + } + } + } + + return filePaths; + } + + + /** + * 创建指定目录 + * + * @param dir 需要创建的目录 例如:/abc/def + * @return boolean + */ + @Override + public boolean createDir(String dir) { + dir = compatibleAbsolutePath(dir); + try { + File file = FileUtil.mkdir(dir); + if (file != null) { + return true; + } + return false; + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, "创建文件夹异常: {}", e); + return false; + } + } + + /** + * 创建多个指定目录 + * + * @param paths + * @return boolean + */ + @Override + public boolean createDirs(String... paths) { + if (null == paths || paths.length < MagicNumConstant.ONE) { + return true; + } + for (String path : paths) { + if (path == null) { + continue; + } + if (!createDir(path)) { + return false; + } + } + return true; + } + + /** + * 指定目录中创建文件 + * + * @param dir 需要创建的目录 例如:/abc/def + * @param fileName 需要创建的文件 例如:dd.txt + * @return boolean + */ + @Override + public boolean createFile(String dir, String fileName) { + try { + File file = FileUtil.touch(dir + SymbolConstant.SLASH + fileName); + if (file != null) { + return true; + } + return false; + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, "创建文件异常: {}", e); + return false; + } + } + + /** + * 新建或追加文件 + * + * @param filePath 文件绝对路径 + * @param content 文件内容 + * @param append 文件是否是追加 + * @return + */ + @Override + public boolean createOrAppendFile(String filePath, String content, boolean append) { + File file = new File(filePath); + FileOutputStream outputStream = null; + try { + if (!file.exists()) { + file.createNewFile(); + } + outputStream = new FileOutputStream(file, append); + outputStream.write(content.getBytes(CharsetUtil.defaultCharset())); + outputStream.flush(); + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, e); + return false; + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, e); + } + } + } + return true; + } + + /** + * 删除目录 或者文件 + * + * @param dirOrFile 需要删除的目录 或者文件 例如:/abc/def 或者 /abc/def/dd.txt + * @return boolean + */ + @Override + public boolean deleteDirOrFile(String dirOrFile) { + return FileUtil.del(dirOrFile); + } + + /** + * 复制文件 到指定目录下 单个文件 + * + * @param sourceFile 需要复制的文件 例如:/abc/def/dd.txt + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + @Override + public boolean copyFile(String sourceFile, String targetPath) { + if (StringUtils.isEmpty(sourceFile) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourceFile = compatibleAbsolutePath(sourceFile); + targetPath = compatibleAbsolutePath(targetPath); + try { + String fileName = sourceFile.substring(sourceFile.lastIndexOf(SymbolConstant.SLASH) + 1); + File file = FileUtil.copy(sourceFile, targetPath + SymbolConstant.SLASH + fileName, true); + if (file != null) { + return true; + } + return false; + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, "复制文件异常: {}", e); + return false; + } + } + + @Override + public boolean copyFile(String sourcePath, String targetPath, Integer type) { + return false; + } + + /** + * 复制路径下文件并重命名 + * + * @param sourceFile 需要复制的文件 例如:/abc/def/dd.txt + * @param targetFile 目标文件全路径 + * @return boolean + */ + @Override + public boolean copyFileAndRename(String sourceFile, String targetFile) { + if (StringUtils.isEmpty(sourceFile) || StringUtils.isEmpty(targetFile)) { + return false; + } + sourceFile = compatibleAbsolutePath(sourceFile); + targetFile = compatibleAbsolutePath(targetFile); + try { + File copyFile = FileUtil.copy(sourceFile, targetFile, true); + if (copyFile != null) { + return true; + } + return false; + } catch (Exception e) { + LogUtil.error(LogEnum.SERVING, " failed to copy file original path: {} ,target path: {} ,exception: {}", sourceFile, targetFile, e); + return false; + } + } + + /** + * 复制路径下文件及文件夹到指定目录下 多个文件 包含目录与文件并存情况 + * + * 通过本地文件复制方式 + * + * @param sourcePath 需要复制的文件目录 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + @Override + public boolean copyPath(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourcePath = compatibleAbsolutePath(sourcePath); + targetPath = compatibleAbsolutePath(targetPath); + try { + return copyLocalPath(sourcePath, targetPath); + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, " failed to Copy file original path: {} ,target path: {} ,Exception: {}", sourcePath, targetPath, e); + return false; + } + } + + /** + * 复制文件夹到指定目录下 + * + * @param sourcePath 需要复制的文件夹 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd 复制成功路径/abc/dd/def* + * @return boolean + */ + @Override + public boolean copyDir(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourcePath = compatibleAbsolutePath(sourcePath); + targetPath = formatPath(compatibleAbsolutePath(targetPath) + FILE_SEPARATOR + new File(sourcePath).getName()); + try { + return copyLocalPath(sourcePath, targetPath); + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, " failed to Copy file original path: {} ,target path: {} ,Exception: {}", sourcePath, targetPath, e); + return false; + } + } + + /** + * 复制文件 到指定目录下 多个文件 包含目录与文件并存情况 + * + * @param sourcePath 需要复制的文件目录 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + private boolean copyLocalPath(String sourcePath, String targetPath) { + if (!StringUtils.isEmpty(sourcePath) && !StringUtils.isEmpty(targetPath)) { + sourcePath = formatPath(sourcePath); + if (sourcePath.endsWith(FILE_SEPARATOR)) { + sourcePath = sourcePath.substring(MagicNumConstant.ZERO, sourcePath.lastIndexOf(FILE_SEPARATOR)); + } + targetPath = formatPath(targetPath); + File sourceFile = new File(sourcePath); + if (sourceFile.exists()) { + File[] files = sourceFile.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + try { + if (file.isDirectory()) { + File fileDir = new File(targetPath + FILE_SEPARATOR + file.getName()); + if (!fileDir.exists()) { + fileDir.mkdirs(); + } + copyLocalPath(sourcePath + FILE_SEPARATOR + file.getName(), targetPath + FILE_SEPARATOR + file.getName()); + } + if (file.isFile()) { + File fileTargetPath = new File(targetPath); + if (!fileTargetPath.exists()) { + fileTargetPath.mkdirs(); + } + copyLocalFile(file.getAbsolutePath(), targetPath + FILE_SEPARATOR + file.getName()); + } + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, "failed to copy folder original path: {} , target path : {} ,Exception: {}", sourcePath, targetPath, e); + return false; + } + } + } + return true; + } + } + return false; + } + + /** + * 复制单个文件到指定目录下 单个文件 + * + * @param sourcePath 需要复制的文件 例如:/abc/def/cc.txt + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + private boolean copyLocalFile(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourcePath = formatPath(sourcePath); + targetPath = formatPath(targetPath); + try (InputStream input = new FileInputStream(sourcePath); + FileOutputStream output = new FileOutputStream(targetPath)) { + FileCopyUtils.copy(input, output); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, " failed to copy file original path: {} ,target path: {} ,Exception:{} ", sourcePath, targetPath, e); + return false; + } + } + + /** + * zip解压并删除压缩文件 + * @param sourceFile zip源文件 例如:/abc/z.zip + */ + @Override + public boolean unzip(String sourceFile, String targetPath) { + if (StringUtils.isEmpty(sourceFile) || StringUtils.isEmpty(targetPath)) { + return false; + } + if (!sourceFile.toLowerCase().endsWith(ZIP)) { + return false; + } + //绝对路径 + String sourceAbsolutePath = getRootDir() + sourceFile; + String targetPathAbsolutePath = getRootDir() + targetPath; + ZipFile zipFile = null; + InputStream in = null; + OutputStream out = null; + File absoluteSourceFile = new File(compatiblePath(sourceAbsolutePath)); + File targetFileDir = new File(compatiblePath(targetPathAbsolutePath)); + if (!targetFileDir.exists()) { + boolean targetMkdir = targetFileDir.mkdirs(); + if (!targetMkdir) { + LogUtil.error(LogEnum.FILE_UTIL, "{} failed to create target folder before decompression", sourceAbsolutePath); + } + } + try { + zipFile = new ZipFile(absoluteSourceFile); + //判断压缩文件编码方式,并重新获取文件对象 + try { + zipFile.close(); + zipFile = new ZipFile(absoluteSourceFile, CHARACTER_GBK); + } catch (Exception e) { + zipFile.close(); + zipFile = new ZipFile(absoluteSourceFile); + LogUtil.error(LogEnum.FILE_UTIL, "{} the encoding mode of decompressed compressed file is changed to UTF-8,Exception:{}", sourceAbsolutePath, e); + } + ZipEntry entry; + Enumeration enumeration = zipFile.getEntries(); + while (enumeration.hasMoreElements()) { + entry = (ZipEntry) enumeration.nextElement(); + String entryName = entry.getName(); + File fileDir; + if (entry.isDirectory()) { + fileDir = new File(targetPathAbsolutePath + entry.getName()); + if (!fileDir.exists()) { + boolean fileMkdir = fileDir.mkdirs(); + if (!fileMkdir) { + LogUtil.error(LogEnum.FILE_UTIL, "failed to create folder {} while decompressing {}", fileDir, sourceAbsolutePath); + } + } + } else { + //若文件夹未创建则创建文件夹 + if (entryName.contains(FILE_SEPARATOR)) { + String zipDirName = entryName.substring(MagicNumConstant.ZERO, entryName.lastIndexOf(FILE_SEPARATOR)); + fileDir = new File(targetPathAbsolutePath + zipDirName); + if (!fileDir.exists()) { + boolean fileMkdir = fileDir.mkdirs(); + if (!fileMkdir) { + LogUtil.error(LogEnum.FILE_UTIL, "failed to create folder {} while decompressing {}", fileDir, sourceAbsolutePath); + } + } + } + in = zipFile.getInputStream((ZipArchiveEntry) entry); + out = new FileOutputStream(new File(targetPathAbsolutePath, entryName)); + org.apache.commons.io.IOUtils.copyLarge(in, out); + in.close(); + out.close(); + } + } + boolean deleteZipFile = absoluteSourceFile.delete(); + if (!deleteZipFile) { + LogUtil.error(LogEnum.FILE_UTIL, "{} compressed file deletion failed after decompression", sourceAbsolutePath); + } + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, "{} decompression failed,Exception: {}", sourceAbsolutePath, e); + return false; + } finally { + //关闭未关闭的io流 + closeIoFlow(sourceAbsolutePath, zipFile, in, out); + } + } + + /** + * windows 与 linux 的路径兼容 + * + * @param path linux下的路径 + * @return path 兼容windows后的路径 + */ + private String compatiblePath(String path) { + if (path == null) { + return null; + } + if (System.getProperties().getProperty(OS_NAME).contains(WINDOWS)) { + path = path.replace(getRootDir(), SymbolConstant.SLASH); + path = path.replace(SymbolConstant.SLASH, FILE_SEPARATOR); + path = rootWindowsPath + path; + } + return path; + } + + /** + * 关闭未关闭的io流 + * + * @param sourceAbsolutePath 源路径 + * @param zipFile 压缩文件对象 + * @param in 输入流 + * @param out 输出流 + */ + private void closeIoFlow(String sourceAbsolutePath, ZipFile zipFile, InputStream in, OutputStream out) { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, "{} input stream shutdown failed,Exception: {}", sourceAbsolutePath, e); + } + } + if (out != null) { + try { + out.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, "{} output stream shutdown failed,Exception: {}", sourceAbsolutePath, e); + } + } + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, "{} input stream shutdown failed,Exception: {}", sourceAbsolutePath, e); + } + } + } + + /** + * 解压压缩包 包含目录与子目录 + * + * @param sourceFile 需要复制的文件 例如:/abc/def/aaa.rar + * @return boolean + */ + @Override + public boolean unzip(String sourceFile) { + try { + File file = ZipUtil.unzip(sourceFile); + if (file != null) { + return true; + } + return false; + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, "解压异常: {}", e); + return false; + } + } + + /** + * 压缩 目录 或者文件 到压缩包 + * + * @param dirOrFile 目录或者文件 例如: /abc/def/aaa.txt , /abc/def + * @param zipPath 压缩包全路径名 例如: /abc/def/aa.zip + * @return boolean + */ + @Override + public boolean zipDirOrFile(String dirOrFile, String zipPath) { + try { + File file = ZipUtil.zip(dirOrFile, zipPath); + if (file != null) { + return true; + } + return false; + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, "压缩异常: {}", e); + return false; + } + } + + @Override + public BufferedInputStream getInputStream(String path) { + return FileUtil.getInputStream(path); + } + + @Override + public void download(String path, HttpServletResponse response) { + if (path == null) { + return; + } + FileInputStream fis = null; + ServletOutputStream out = null; + try { + File file = new File(path); + fis = new FileInputStream(file); + out = response.getOutputStream(); + IOUtils.copy(fis, out); + response.flushBuffer(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + //此处记得关闭输出Servlet流 + IoUtil.close(out); + } + } + + @Override + public void filterFilePageWithPath(FilePageDTO filePageDTO) { + if (ObjectUtil.isEmpty(filePageDTO) || StringUtils.isEmpty(filePageDTO.getFilePath())) { + return; + } + File file = new File(filePageDTO.getFilePath()); + if (!file.exists()) { + return; + } + File[] files = file.listFiles(); + if (files.length > 0) { + filePageDTO.setTotal(Long.valueOf(files.length)); + int start = (filePageDTO.getPageNum() - 1) * filePageDTO.getPageSize(); + int end = filePageDTO.getPageNum() * filePageDTO.getPageSize(); + List fileDTOS = new ArrayList<>(); + for (int i = (start <= files.length ? start : files.length); i < (end <= files.length ? end : files.length); i++) { + FileDTO fileDTO = FileDTO.builder().name(files[i].getName()) + .path(files[i].getAbsolutePath()) + .lastModified(new Date(files[i].lastModified())) + .size(files[i].length()) + .dir(files[i].isDirectory()).build(); + fileDTOS.add(fileDTO); + } + filePageDTO.setRows(fileDTOS); + } + } +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/NfsFileStoreApiImpl.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/NfsFileStoreApiImpl.java new file mode 100644 index 0000000..ddbc792 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/NfsFileStoreApiImpl.java @@ -0,0 +1,152 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.api.impl; + +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.dto.FilePageDTO; +import org.dubhe.biz.file.utils.NfsUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedInputStream; +import java.util.List; + +/** + * @description Nfs文件存储接口 + * @date 2021-04-20 + */ +@Deprecated +@Component(value = "nfsFileStoreApiImpl") +public class NfsFileStoreApiImpl implements FileStoreApi { + + @Autowired + private NfsUtil nfsUtil; + + @Value("${storage.file-store-root-path}") + private String rootDir; + + @Value("/${minio.bucketName}/") + private String bucket; + + @Override + public String getRootDir(){ + return rootDir; + } + + @Override + public String getBucket(){ + return bucket; + } + + @Override + public boolean fileOrDirIsExist(String path) { + return nfsUtil.fileOrDirIsEmpty(path); + } + + @Override + public boolean isDirectory(String path) { + return false; + } + + @Override + public List filterFileSuffix(String path, String fileSuffix) { + return null; + } + + @Override + public boolean createDir(String dir) { + return nfsUtil.createDir(dir); + } + + @Override + public boolean createDirs(String... paths) { + return nfsUtil.createDirs(true,paths); + } + + @Override + public boolean createFile(String dir, String fileName) { + return nfsUtil.createFile(dir,fileName); + } + + @Override + public boolean createOrAppendFile(String filePath, String content, boolean append) { + return false; + } + + @Override + public boolean deleteDirOrFile(String dirOrFile) { + return nfsUtil.deleteDirOrFile(dirOrFile); + } + + @Override + public boolean copyFile(String sourceFile, String targetPath) { + return nfsUtil.copyFile(sourceFile,targetPath); + } + + @Override + public boolean copyFile(String sourceFile, String targetPath, Integer type) { + return false; + } + + @Override + public boolean copyFileAndRename(String sourceFile, String targetFile) { + return false; + } + + @Override + public boolean copyPath(String sourcePath, String targetPath) { + return nfsUtil.copyNfsPath(sourcePath, targetPath); + } + + @Override + public boolean copyDir(String sourcePath, String targetPath) { + return false; + } + + @Override + public boolean unzip(String sourceFile, String targetPath) { + return nfsUtil.unzip(sourceFile,targetPath); + } + + @Override + public boolean unzip(String sourceFile) { + return nfsUtil.unZip(sourceFile); + } + + @Override + public boolean zipDirOrFile(String dirOrFile, String zipName) { + return nfsUtil.zipDirOrFile(dirOrFile,zipName); + } + + @Override + public BufferedInputStream getInputStream(String path) { + return nfsUtil.getInputStream(path); + } + + @Override + public void download(String path, HttpServletResponse response) { + return; + } + + @Override + public void filterFilePageWithPath(FilePageDTO filePageDTO) { + + } +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/ShellFileStoreApiImpl.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/ShellFileStoreApiImpl.java new file mode 100644 index 0000000..74fc431 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/api/impl/ShellFileStoreApiImpl.java @@ -0,0 +1,267 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.file.api.impl; + +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.dto.FilePageDTO; +import org.dubhe.biz.file.enums.CopyTypeEnum; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.util.List; +import java.util.Objects; + +/** + * @description 通过shell指令存储文件接口实现类 + * @date 2021-05-06 + */ +@Deprecated +@Component(value = "shellFileStoreApiImpl") +public class ShellFileStoreApiImpl implements FileStoreApi { + + /** + * 服务暴露的IP地址 + */ + @Value("${storage.file-store}") + private String ip; + + /** + * 文件存储服务器用户名 + */ + @Value("${data.server.userName}") + private String userName; + + /** + * 拷贝源路径下文件或文件夹命令 + */ + public static final String COPY_COMMAND = "ssh %s@%s \"mkdir -p %s && cp -rf %s %s && echo success\""; + /** + * 拷贝多文件夹下文件命令 + */ + public static final String COPY_DIR_COMMAND = "ssh %s@%s \"mkdir -p %s && cp -rf %s* %s && echo success\""; + + /** + * 删除服务器无效文件(大文件) + * 示例:rsync --delete-before -d /空目录 /需要回收的源目录 + */ + public static final String DEL_COMMAND = "ssh %s@%s \"mkdir -p %s; rsync --delete-before -d %s %s; rmdir %s %s\""; + + /** + * 拷贝文件并重命名 + */ + public static final String COPY_RENAME_COMMAND = "ssh %s@%s \"cp -rf %s %s && echo success\""; + + /** + * 文件复制 + * rsync -avP --exclude={'dir'} sourcePath targetPath 将原路径复制到目标路径下,过滤dir目录 + * 示例:rsync -avP --exclude={'V0001'} /root/test/ /root/test2/ + */ + public static final String COPY_AVP_COMMAND = "ssh %s@%s rsync -avP --exclude={'%s'} %s %s"; + + /** + * 修改文件夹名称 + * mv sourcePathName targetPathName 将原目录名称修改为目标目录名称 + * 示例:mv /root/test2/versionFile/V0002 /root/test2/versionFile/V0001 + */ + public static final String UPDATE_NAME_COMMAND = "ssh %s@%s mv %s %s"; + + @Value("${storage.file-store-root-path}") + private String rootDir; + + @Value("/${minio.bucketName}/") + private String bucket; + + @Override + public String getRootDir() { + return rootDir; + } + + @Override + public String getBucket() { + return bucket; + } + + @Override + public boolean fileOrDirIsExist(String path) { + return false; + } + + @Override + public boolean isDirectory(String path) { + return false; + } + + @Override + public List filterFileSuffix(String path, String fileSuffix) { + return null; + } + + @Override + public boolean createDir(String dir) { + return false; + } + + @Override + public boolean createDirs(String... paths) { + return false; + } + + @Override + public boolean createFile(String dir, String fileName) { + return false; + } + + @Override + public boolean createOrAppendFile(String filePath, String content, boolean append) { + return false; + } + + @Override + public boolean deleteDirOrFile(String dirOrFile) { + return false; + } + + @Override + public boolean copyFile(String sourceFile, String targetPath) { + return false; + } + + /** + * 使用shell拷贝文件或路径 + * + * @param sourcePath 需要复制的文件或路径 例如:/abc/def/cc.txt or /abc/def* + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @param type CopyTypeEnum的 + * @return boolean + */ + @Override + public boolean copyFile(String sourcePath, String targetPath, Integer type) { + //绝对路径 + String sourceAbsolutePath = formatPath(getRootDir() + sourcePath); + String targetPathAbsolutePath = formatPath(getRootDir() + targetPath); + String[] command; + if (CopyTypeEnum.COPY_FILE.getKey().equals(type)) { + command = new String[]{"/bin/sh", "-c", String.format(COPY_COMMAND, userName, ip, targetPathAbsolutePath, sourceAbsolutePath, targetPathAbsolutePath)}; + } else { + command = new String[]{"/bin/sh", "-c", String.format(COPY_DIR_COMMAND, userName, ip, targetPathAbsolutePath, sourceAbsolutePath, targetPathAbsolutePath)}; + } + boolean flag = false; + Process process; + try { + process = Runtime.getRuntime().exec(command); + if (isCopySuccess(process)) { + flag = true; + } + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, "copy file failed, filePath:{}, targetPath:{}", sourcePath, targetPath, e); + } + return flag; + } + + /** + * 判断拷贝结果 + * + * @param process + * @return + */ + public boolean isCopySuccess(Process process) { + try (InputStream stream = process.getInputStream(); + InputStreamReader iReader = new InputStreamReader(stream); + BufferedReader bReader = new BufferedReader(iReader)) { + String line; + while (Objects.nonNull(line = bReader.readLine())) { + boolean temp = line.contains("success"); + if (temp) { + return true; + } + } + } catch (Exception e) { + LogUtil.error(LogEnum.FILE_UTIL, "Read stream failed : {}", e); + } + return false; + } + + /** + * 拷贝文件并重命名 + * + * @param sourceFile 需要复制的文件 例如:/abc/def/aa.py + * @param targetFile 需要放置的目标目录 例如:/abc/dd/bb.py + * @return + */ + @Override + public boolean copyFileAndRename(String sourceFile, String targetFile) { + //绝对路径 + String sourceAbsolutePath = formatPath(rootDir + sourceFile); + String targetPathAbsolutePath = formatPath(rootDir + targetFile); + String[] command = new String[]{"/bin/sh", "-c", String.format(COPY_RENAME_COMMAND, userName, ip, sourceAbsolutePath, targetPathAbsolutePath)}; + boolean flag = false; + Process process; + try { + process = Runtime.getRuntime().exec(command); + if (isCopySuccess(process)) { + flag = true; + } + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL, "copy file failed, filePath:{}, targetPath:{}, because:{}", sourceFile, targetFile, e); + } + return flag; + } + + @Override + public boolean copyPath(String sourcePath, String targetPath) { + return false; + } + + @Override + public boolean copyDir(String sourcePath, String targetPath) { + return false; + } + + @Override + public boolean unzip(String sourceFile, String targetPath) { + return false; + } + + @Override + public boolean unzip(String sourceFile) { + return false; + } + + @Override + public boolean zipDirOrFile(String dirOrFile, String zipPath) { + return false; + } + + @Override + public BufferedInputStream getInputStream(String path) { + return null; + } + + @Override + public void download(String path, HttpServletResponse response) { + + } + + @Override + public void filterFilePageWithPath(FilePageDTO filePageDTO) { + + } +} \ No newline at end of file diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/config/NfsConfig.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/config/NfsConfig.java new file mode 100644 index 0000000..5d269e4 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/config/NfsConfig.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @description NFS config + * @date 2020-05-13 + */ +@Data +@Component +public class NfsConfig { + + @Value("${storage.file-store}") + private String nfsIp; + + @Value("${storage.file-store-root-path}") + private String rootDir; + + @Value("/${minio.bucketName}/") + private String bucket; + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FileDTO.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FileDTO.java new file mode 100644 index 0000000..50bc603 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FileDTO.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.dto; + +import lombok.Builder; +import lombok.Data; +import java.io.Serializable; +import java.util.Date; + +/** + * @description 文件详情 + * @date 2021-05-07 + */ +@Builder +@Data +public class FileDTO implements Serializable { + + /** + * 文件名称 + */ + private String name; + /** + * 文件路径 + */ + private String path; + /** + * 文件最近一次修改时间 + */ + private Date lastModified; + /** + * 文件大小 + */ + private long size; + /** + * 是否文件夹 + */ + private boolean dir; + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FilePageDTO.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FilePageDTO.java new file mode 100644 index 0000000..d381750 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/FilePageDTO.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.dto; + +import lombok.Data; +import java.util.List; + +/** + * @description 文件分页查询相应实体 + * @date 2021-06-16 + */ +@Data +public class FilePageDTO { + + /** + * 查询路径 + */ + private String filePath; + /** + * 页码 + */ + private int pageNum; + /** + * 页容量 + */ + private int pageSize; + /** + * 记录数 + */ + private Long total; + /** + * 页集合 + */ + private List rows; + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/MinioDownloadDTO.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/MinioDownloadDTO.java new file mode 100644 index 0000000..5c4ec21 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/dto/MinioDownloadDTO.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.dto; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +/** + * @description minio下载参数实体 + * @date 2021-06-24 + */ +@Data +public class MinioDownloadDTO { + /** + * 下载压缩包请求token + */ + private String token; + /** + * 下载压缩包请求参数 + */ + private String body; + /** + * 下载压缩包请求需要的header + */ + private Map headers; + /** + * 下载压缩包文件名称 + */ + private String zipName; + + public MinioDownloadDTO() { + } + + public MinioDownloadDTO(String token, String body, String zipName) { + this.token = token; + this.body = body; + this.zipName = zipName; + Map headers = new HashMap<>(); + headers.put("Content-Type", "text/plain;charset=UTF-8"); + this.headers = headers; + } + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/BizPathEnum.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/BizPathEnum.java new file mode 100644 index 0000000..ba5c5c3 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/BizPathEnum.java @@ -0,0 +1,105 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.enums; + +import org.dubhe.biz.base.enums.BizEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * @description 业务NFS路径枚举 + * @date 2020-05-13 + */ +public enum BizPathEnum { + /** + * 模型开发 路径命名 + */ + NOTEBOOK(BizEnum.NOTEBOOK, "notebook"), + /** + * 算法管理 路径命名 + */ + ALGORITHM(BizEnum.ALGORITHM, "algorithm-manage"), + /** + * 模型管理 路径命名 + */ + MODEL(BizEnum.MODEL, "model"), + /** + * 模型优化 路径命名 + */ + MODEL_OPT(BizEnum.MODEL_OPT, "model-opt"), + + /** + * 度量管理 路径命名 + */ + MEASURE(BizEnum.MEASURE, "exported-metrics"); + + BizPathEnum(BizEnum bizEnum, String bizPath) { + this.bizEnum = bizEnum; + this.bizPath = bizPath; + } + + /** + * 业务模块 + */ + private BizEnum bizEnum; + /** + * 业务模块路径 + */ + private String bizPath; + + + private static final Map RESOURCE_ENUM_MAP = new HashMap() { + { + for (BizPathEnum enums : BizPathEnum.values()) { + put(enums.getCreateResource(), enums); + } + } + }; + + /** + * 根据createResource获取BizEnum + * + * @param createResource + * @return + */ + public static BizPathEnum getByCreateResource(int createResource) { + return RESOURCE_ENUM_MAP.get(createResource); + } + + + public String getBizName() { + return bizEnum == null ? null : bizEnum.getBizName(); + } + + public Integer getCreateResource() { + return bizEnum == null ? null : bizEnum.getCreateResource(); + } + + public String getBizPath() { + return bizPath; + } + + public BizEnum getBizEnum() { + return bizEnum; + } + + public String getBizCode() { + return bizEnum == null ? null : bizEnum.getBizCode(); + } +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/CopyTypeEnum.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/CopyTypeEnum.java new file mode 100644 index 0000000..149a3e9 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/enums/CopyTypeEnum.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.enums; + +/** + * @description 文件复制类型枚举 + * @date 2021-01-21 + */ +public enum CopyTypeEnum { + + COPY_FILE(0,"拷贝文件"), + COPY_DIR(1,"拷贝文件夹内文件") + ; + + private Integer key; + + private String value; + + CopyTypeEnum(Integer key, String value) { + this.key = key; + this.value = value; + } + + public Integer getKey() { + return key; + } + + public void setKey(Integer key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/exception/NfsBizException.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/exception/NfsBizException.java new file mode 100644 index 0000000..a5654eb --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/exception/NfsBizException.java @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.exception; + +import lombok.Getter; +import org.dubhe.biz.base.exception.BusinessException; + +/** + * @description NFS utils 工具异常 + * @date 2020-06-15 + */ +@Getter +public class NfsBizException extends BusinessException { + + private static final long serialVersionUID = 1L; + + + public NfsBizException(Throwable cause){ + super(cause); + } + + public NfsBizException(String msg){ + super(msg); + } + + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/DubheFileUtil.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/DubheFileUtil.java new file mode 100644 index 0000000..25e06a3 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/DubheFileUtil.java @@ -0,0 +1,410 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.utils; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.poi.excel.BigExcelWriter; +import cn.hutool.poi.excel.ExcelUtil; +import org.apache.poi.util.IOUtils; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @description File工具类,扩展 hutool 工具包 + * @date 2020-03-14 + */ +public class DubheFileUtil extends cn.hutool.core.io.FileUtil { + + /** + * 定义GB的计算常量 + */ + private static final int GB = 1024 * 1024 * 1024; + /** + * 定义MB的计算常量 + */ + private static final int MB = 1024 * 1024; + /** + * 定义KB的计算常量 + */ + private static final int KB = 1024; + + /** + * 格式化小数 + */ + private static final DecimalFormat DF = new DecimalFormat("0.00"); + + /** + * MultipartFile转File + */ + public static File toFile(MultipartFile multipartFile) { + // 获取文件名 + String fileName = multipartFile.getOriginalFilename(); + // 获取文件后缀 + String prefix = "." + getExtensionName(fileName); + File file = null; + try { + // 用uuid作为文件名,防止生成的临时文件重复 + file = File.createTempFile(IdUtil.simpleUUID(), prefix); + // MultipartFile to File + multipartFile.transferTo(file); + } catch (IOException e) { + e.printStackTrace(); + } + return file; + } + + /** + * 获取文件扩展名,不带 . + */ + public static String getExtensionName(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length() - 1))) { + return filename.substring(dot + 1); + } + } + return filename; + } + + /** + * Java文件操作 获取不带扩展名的文件名 + */ + public static String getFileNameNoEx(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length()))) { + return filename.substring(0, dot); + } + } + return filename; + } + + /** + * 文件大小转换 + */ + public static String getSize(long size) { + String resultSize; + if (size / GB >= 1) { + //如果当前Byte的值大于等于1GB + resultSize = DF.format(size / (float) GB) + "GB "; + } else if (size / MB >= 1) { + //如果当前Byte的值大于等于1MB + resultSize = DF.format(size / (float) MB) + "MB "; + } else if (size / KB >= 1) { + //如果当前Byte的值大于等于1KB + resultSize = DF.format(size / (float) KB) + "KB "; + } else { + resultSize = size + "B "; + } + return resultSize; + } + + /** + * inputStream 转 File + */ + static File inputStreamToFile(InputStream ins, String name) throws Exception { + File file = new File(System.getProperty("java.io.tmpdir") + File.separator + name); + if (file.exists()) { + return file; + } + OutputStream os = new FileOutputStream(file); + int bytesRead; + int len = 8192; + byte[] buffer = new byte[len]; + while ((bytesRead = ins.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + return file; + } + + /** + * 将文件名解析成文件的上传路径 + */ + public static File upload(MultipartFile file, String filePath) { + Date date = new Date(); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS"); + String name = getFileNameNoEx(file.getOriginalFilename()); + String suffix = getExtensionName(file.getOriginalFilename()); + String nowStr = "-" + format.format(date); + try { + String fileName = name + nowStr + "." + suffix; + String path = filePath + fileName; + // getCanonicalFile 可解析正确各种路径 + File dest = new File(path).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + dest.getParentFile().mkdirs(); + } + // 文件写入 + file.transferTo(dest); + return dest; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String fileToBase64(File file) throws Exception { + FileInputStream inputFile = new FileInputStream(file); + String base64; + byte[] buffer = new byte[(int) file.length()]; + inputFile.read(buffer); + inputFile.close(); + base64 = Base64.encode(buffer); + return base64.replaceAll("[\\s*\t\n\r]", ""); + } + + /** + * 导出excel + */ + public static void downloadExcel(List> list, HttpServletResponse response) throws IOException { + String tempPath = System.getProperty("java.io.tmpdir") + IdUtil.fastSimpleUUID() + ".xlsx"; + File file = new File(tempPath); + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // 一次性写出内容,使用默认样式,强制输出标题 + writer.write(list, true); + //response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename=file.xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + //此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + /** + * 下载文件 + */ + public static void download(String path, HttpServletResponse response) { + if (path == null) { + return; + } + FileInputStream fis = null; + ServletOutputStream out = null; + try { + File file = new File(path); + fis = new FileInputStream(file); + out = response.getOutputStream(); + IOUtils.copy(fis, out); + response.flushBuffer(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + //此处记得关闭输出Servlet流 + IoUtil.close(out); + } + } + + public static String getFileType(String type) { + String documents = "txt doc pdf ppt pps xlsx xls docx"; + String music = "mp3 wav wma mpa ram ra aac aif m4a"; + String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; + String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; + if (image.contains(type)) { + return "图片"; + } else if (documents.contains(type)) { + return "文档"; + } else if (music.contains(type)) { + return "音乐"; + } else if (video.contains(type)) { + return "视频"; + } else { + return "其他"; + } + } + + public static void checkSize(long maxSize, long size) { + // 1M + int len = 1024 * 1024; + if (size > (maxSize * len)) { + throw new BusinessException("文件超出规定大小"); + } + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(File file1, File file2) { + String img1Md5 = getMd5(file1); + String img2Md5 = getMd5(file2); + return img1Md5.equals(img2Md5); + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(String file1Md5, String file2Md5) { + return file1Md5.equals(file2Md5); + } + + private static byte[] getByte(File file) { + // 得到文件长度 + byte[] b = new byte[(int) file.length()]; + try { + InputStream in = new FileInputStream(file); + try { + in.read(b); + } catch (IOException e) { + e.printStackTrace(); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + return b; + } + + private static String getMd5(byte[] bytes) { + // 16进制字符 + char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + try { + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + mdTemp.update(bytes); + byte[] md = mdTemp.digest(); + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + // 移位 输出字符串 + for (byte byte0 : md) { + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 下载文件 + * + * @param request / + * @param response / + * @param file / + */ + public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file, boolean deleteOnExit) { + response.setCharacterEncoding(request.getCharacterEncoding()); + response.setContentType("application/octet-stream"); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + response.setHeader("Content-Disposition", "attachment; filename=" + file.getName()); + IOUtils.copy(fis, response.getOutputStream()); + response.flushBuffer(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (fis != null) { + try { + fis.close(); + if (deleteOnExit) { + file.deleteOnExit(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static String getMd5(File file) { + return getMd5(getByte(file)); + } + + + /** + * 生成文件 + * @param filePath 文件绝对路径 + * @param content 文件内容 + * @param append 文件是否是追加 + * @return + */ + public static boolean generateFile(String filePath,String content,boolean append){ + File file = new File(filePath); + FileOutputStream outputStream = null; + try { + if (!file.exists()){ + file.createNewFile(); + } + outputStream = new FileOutputStream(file,append); + outputStream.write(content.getBytes(CharsetUtil.defaultCharset())); + outputStream.flush(); + }catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL,e); + return false; + }finally { + if (outputStream != null){ + try { + outputStream.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.FILE_UTIL,e); + } + } + } + return true; + } + + + /** + * 压缩文件目录 + * + * @param zipDir 待压缩文件夹路径 + * @param zipFile 压缩完成zip文件绝对路径 + * @return + */ + public static boolean zipPath(String zipDir,String zipFile) { + if (zipDir == null) { + return false; + } + File zip = new File(zipFile); + cn.hutool.core.util.ZipUtil.zip(zip, CharsetUtil.defaultCharset(), true, + (f) -> !f.isDirectory(), + new File(zipDir).listFiles()); + return true; + } + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/IOUtil.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/IOUtil.java new file mode 100644 index 0000000..51b09e6 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/IOUtil.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.file.utils; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; + +import java.io.Closeable; +import java.io.IOException; + +/** + * @description IO流操作工具类 + * @date 2020-10-14 + */ +public class IOUtil { + + /** + * 循环的依次关闭流 + * + * @param closeableList 要被关闭的流集合 + */ + public static void close(Closeable... closeableList) { + for (Closeable closeable : closeableList) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException e) { + LogUtil.error(LogEnum.IO_UTIL, "关闭流异常,异常信息:{}", e); + } + } + } +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/LocalFileUtil.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/LocalFileUtil.java new file mode 100644 index 0000000..22fd076 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/LocalFileUtil.java @@ -0,0 +1,434 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.file.utils; + +import cn.hutool.core.util.StrUtil; +import lombok.Getter; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.io.IOUtils; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.config.NfsConfig; +import org.dubhe.biz.file.enums.CopyTypeEnum; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.FileCopyUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.Objects; +import java.util.zip.ZipEntry; + +/** + * @description 本地文件操作工具类 + * @date 2020-08-19 + */ +@Component +@Getter +public class LocalFileUtil { + + @Autowired + private NfsConfig nfsConfig; + + private static final String FILE_SEPARATOR = File.separator; + + private static final String ZIP = ".zip"; + + private static final String CHARACTER_GBK = "GBK"; + + private static final String OS_NAME = "os.name"; + + private static final String WINDOWS = "Windows"; + + /** + * nfs服务暴露的IP地址 + */ + @Value("${storage.file-store}") + private String nfsIp; + + /** + * 文件存储服务器用户名 + */ + @Value("${data.server.userName}") + private String userName; + + /** + * 拷贝源路径下文件或文件夹命令 + */ + public static final String COPY_COMMAND = "ssh %s@%s \"mkdir -p %s && cp -rf %s %s && echo success\""; + /** + * 拷贝多文件夹下文件命令 + */ + public static final String COPY_DIR_COMMAND = "ssh %s@%s \"mkdir -p %s && cp -rf %s* %s && echo success\""; + /** + * 拷贝文件并重命名 + */ + public static final String COPY_RENAME_COMMAND = "ssh %s@%s \"cp -rf %s %s && echo success\""; + + @Value("${storage.file-store-root-path}") + private String nfsRootPath; + + @Value("${storage.file-store-root-windows-path}") + private String nfsRootWindowsPath; + + /** + * windows 与 linux 的路径兼容 + * + * @param path linux下的路径 + * @return path 兼容windows后的路径 + */ + private String compatiblePath(String path) { + if (path == null) { + return null; + } + if (System.getProperties().getProperty(OS_NAME).contains(WINDOWS)) { + path = path.replace(nfsRootPath, StrUtil.SLASH); + path = path.replace(StrUtil.SLASH, FILE_SEPARATOR); + path = nfsRootWindowsPath + path; + } + return path; + } + + + /** + * 本地解压zip包并删除压缩文件 + * + * @param sourcePath zip源文件 例如:/abc/z.zip + * @param targetPath 解压后的目标文件夹 例如:/abc/ + * @return boolean + */ + public boolean unzipLocalPath(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + if (!sourcePath.toLowerCase().endsWith(ZIP)) { + return false; + } + //绝对路径 + String sourceAbsolutePath = nfsConfig.getRootDir() + sourcePath; + String targetPathAbsolutePath = nfsConfig.getRootDir() + targetPath; + ZipFile zipFile = null; + InputStream in = null; + OutputStream out = null; + File sourceFile = new File(compatiblePath(sourceAbsolutePath)); + File targetFileDir = new File(compatiblePath(targetPathAbsolutePath)); + if (!targetFileDir.exists()) { + boolean targetMkdir = targetFileDir.mkdirs(); + if (!targetMkdir) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "{}failed to create target folder before decompression", sourceAbsolutePath); + } + } + try { + zipFile = new ZipFile(sourceFile); + //判断压缩文件编码方式,并重新获取文件对象 + try { + zipFile.close(); + zipFile = new ZipFile(sourceFile, CHARACTER_GBK); + } catch (Exception e) { + zipFile.close(); + zipFile = new ZipFile(sourceFile); + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "{}the encoding mode of decompressed compressed file is changed to UTF-8:{}", sourceAbsolutePath, e); + } + ZipEntry entry; + Enumeration enumeration = zipFile.getEntries(); + while (enumeration.hasMoreElements()) { + entry = (ZipEntry) enumeration.nextElement(); + String entryName = entry.getName(); + File fileDir; + if (entry.isDirectory()) { + fileDir = new File(targetPathAbsolutePath + entry.getName()); + if (!fileDir.exists()) { + boolean fileMkdir = fileDir.mkdirs(); + if (!fileMkdir) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "failed to create folder {} while decompressing {}", fileDir, sourceAbsolutePath); + } + } + } else { + //若文件夹未创建则创建文件夹 + if (entryName.contains(FILE_SEPARATOR)) { + String zipDirName = entryName.substring(MagicNumConstant.ZERO, entryName.lastIndexOf(FILE_SEPARATOR)); + fileDir = new File(targetPathAbsolutePath + zipDirName); + if (!fileDir.exists()) { + boolean fileMkdir = fileDir.mkdirs(); + if (!fileMkdir) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "failed to create folder {} while decompressing {}", fileDir, sourceAbsolutePath); + } + } + } + in = zipFile.getInputStream((ZipArchiveEntry) entry); + out = new FileOutputStream(new File(targetPathAbsolutePath, entryName)); + IOUtils.copyLarge(in, out); + in.close(); + out.close(); + } + } + boolean deleteZipFile = sourceFile.delete(); + if (!deleteZipFile) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "{}compressed file deletion failed after decompression", sourceAbsolutePath); + } + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "{}decompression failed: {}", sourceAbsolutePath, e); + return false; + } finally { + //关闭未关闭的io流 + closeIoFlow(sourceAbsolutePath, zipFile, in, out); + } + + } + + /** + * 关闭未关闭的io流 + * + * @param sourceAbsolutePath 源路径 + * @param zipFile 压缩文件对象 + * @param in 输入流 + * @param out 输出流 + */ + private void closeIoFlow(String sourceAbsolutePath, ZipFile zipFile, InputStream in, OutputStream out) { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "{}input stream shutdown failed: {}", sourceAbsolutePath, e); + } + } + if (out != null) { + try { + out.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "{}output stream shutdown failed: {}", sourceAbsolutePath, e); + } + } + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "{}input stream shutdown failed: {}", sourceAbsolutePath, e); + } + } + } + + + /** + * 复制单个文件到指定目录下 单个文件 + * + * @param sourcePath 需要复制的文件 例如:/abc/def/cc.txt + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + private boolean copyLocalFile(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourcePath = formatPath(sourcePath); + targetPath = formatPath(targetPath); + try (InputStream input = new FileInputStream(sourcePath); + FileOutputStream output = new FileOutputStream(targetPath)) { + FileCopyUtils.copy(input, output); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, " failed to copy file original path: {} ,target path: {} ,copyLocalFile:{} ", sourcePath, targetPath, e); + return false; + } + } + + /** + * NFS 复制目录到指定目录下 多个文件 包含目录与文件并存情况 + * + * 通过本地文件复制方式 + * + * @param sourcePath 需要复制的文件目录 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + public boolean copyPath(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourcePath = formatPath(sourcePath); + targetPath = formatPath(targetPath); + try { + return copyLocalPath(nfsConfig.getRootDir() + sourcePath, nfsConfig.getRootDir() + targetPath); + } catch (Exception e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, " failed to Copy file original path: {} ,target path: {} ,copyPath: {}", sourcePath, targetPath, e); + return false; + } + } + + /** + * 复制文件 到指定目录下 多个文件 包含目录与文件并存情况 + * + * @param sourcePath 需要复制的文件目录 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + private boolean copyLocalPath(String sourcePath, String targetPath) { + if (!StringUtils.isEmpty(sourcePath) && !StringUtils.isEmpty(targetPath)) { + sourcePath = formatPath(sourcePath); + if (sourcePath.endsWith(FILE_SEPARATOR)) { + sourcePath = sourcePath.substring(MagicNumConstant.ZERO, sourcePath.lastIndexOf(FILE_SEPARATOR)); + } + targetPath = formatPath(targetPath); + File sourceFile = new File(sourcePath); + if (sourceFile.exists()) { + File[] files = sourceFile.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + try { + if (file.isDirectory()) { + File fileDir = new File(targetPath + FILE_SEPARATOR + file.getName()); + if (!fileDir.exists()) { + fileDir.mkdirs(); + } + copyLocalPath(sourcePath + FILE_SEPARATOR + file.getName(), targetPath + FILE_SEPARATOR + file.getName()); + } + if (file.isFile()) { + File fileTargetPath = new File(targetPath); + if (!fileTargetPath.exists()) { + fileTargetPath.mkdirs(); + } + copyLocalFile(file.getAbsolutePath(), targetPath + FILE_SEPARATOR + file.getName()); + } + } catch (Exception e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "failed to copy folder original path: {} , target path : {} ,copyLocalPath: {}", sourcePath, targetPath, e); + return false; + } + } + } + return true; + } + } + return false; + } + + /** + * 替换路径中多余的 "/" + * + * @param path + * @return String + */ + public String formatPath(String path) { + if (!StringUtils.isEmpty(path)) { + return path.replaceAll("///*", FILE_SEPARATOR); + } + return path; + } + + /** + * 拷贝文件 + * + * @param sourcePath 需要复制的文件 例如:/abc/def/cc.txt + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return + */ + public boolean copyFile(String sourcePath, String targetPath, Integer type) { + //绝对路径 + String sourceAbsolutePath = formatPath(nfsConfig.getRootDir() + sourcePath); + String targetPathAbsolutePath = formatPath(nfsConfig.getRootDir() + targetPath); + String[] command; + if (CopyTypeEnum.COPY_FILE.getKey().equals(type)) { + command = new String[]{"/bin/sh", "-c", String.format(COPY_COMMAND, userName, nfsIp, targetPathAbsolutePath, sourceAbsolutePath, targetPathAbsolutePath)}; + } else { + command = new String[]{"/bin/sh", "-c", String.format(COPY_DIR_COMMAND, userName, nfsIp, targetPathAbsolutePath, sourceAbsolutePath, targetPathAbsolutePath)}; + } + boolean flag = false; + Process process; + try { + process = Runtime.getRuntime().exec(command); + if (isCopySuccess(process)) { + flag = true; + } + } catch (IOException e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "copy file failed, filePath:{}, targetPath:{}", sourcePath, targetPath, e); + } + return flag; + } + + /** + * 拷贝文件并重命名 + * + * @param sourcePath 需要复制的文件 例如:/abc/def/aa.py + * @param targetPath 需要放置的目标目录 例如:/abc/dd/bb.py + * @return + */ + public boolean copyFileAndRename(String sourcePath, String targetPath) { + //绝对路径 + String sourceAbsolutePath = formatPath(nfsConfig.getRootDir() + sourcePath); + String targetPathAbsolutePath = formatPath(nfsConfig.getRootDir() + targetPath); + String[] command = new String[]{"/bin/sh", "-c", String.format(COPY_RENAME_COMMAND, userName, nfsIp, sourceAbsolutePath, targetPathAbsolutePath)}; + boolean flag = false; + Process process; + try { + process = Runtime.getRuntime().exec(command); + if (isCopySuccess(process)) { + flag = true; + } + } catch (IOException e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "copy file failed, filePath:{}, targetPath:{}", sourcePath, targetPath, e); + } + return flag; + } + + /** + * 判断拷贝结果 + * + * @param process + * @return + */ + public boolean isCopySuccess(Process process) { + boolean flag = false; + try (InputStream stream = process.getInputStream(); + InputStreamReader iReader = new InputStreamReader(stream); + BufferedReader bReader = new BufferedReader(iReader)) { + String line; + while (Objects.nonNull(line = bReader.readLine())) { + boolean temp = line.contains("success"); + if (temp) { + flag = true; + } + } + } catch (Exception e) { + LogUtil.error(LogEnum.LOCAL_FILE_UTIL, "Read stream failed : {}", e); + } + return flag; + } + + /** + * 路径是否存在 + * @param path 路径 + * @return true 存在 false 不存在 + */ + public boolean isExist(String path) { + if(StringUtils.isBlank(path)){ + return false; + } + File file = new File(path); + return file.exists(); + } +} \ No newline at end of file diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioUtil.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioUtil.java new file mode 100644 index 0000000..c73e6ca --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioUtil.java @@ -0,0 +1,283 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.utils; + +import cn.hutool.core.io.IoUtil; +import io.minio.CopyConditions; +import io.minio.MinioClient; +import io.minio.PutObjectOptions; +import io.minio.Result; +import io.minio.errors.InvalidEndpointException; +import io.minio.errors.InvalidPortException; +import io.minio.messages.DeleteError; +import io.minio.messages.Item; +import org.apache.commons.lang.StringUtils; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.file.dto.FileDTO; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.*; + +/** + * @description Minio工具类 + * @date 2020-05-09 + */ +@Service +public class MinioUtil { + + @Value("${minio.url}") + private String url; + @Value("${minio.accessKey}") + private String accessKey; + @Value("${minio.secretKey}") + private String secretKey; + + private MinioClient client; + + @PostConstruct + public void init() { + try { + client = new MinioClient(url, accessKey, secretKey); + } catch (InvalidEndpointException e) { + LogUtil.warn(LogEnum.BIZ_DATASET, "MinIO endpoint invalid. e, {}", e); + } catch (InvalidPortException e) { + LogUtil.warn(LogEnum.BIZ_DATASET, "MinIO endpoint port invalid. e, {}", e); + } + } + + /** + * 写文件 + * + * @param bucket 桶名称 + * @param fullFilePath 文件存储的全路径,包括文件名,非'/'开头. e.g. dataset/12/annotation/test.txt + * @param content file content. can not be null + */ + public void writeString(String bucket, String fullFilePath, String content) throws Exception { + boolean isExist = client.bucketExists(bucket); + if (!isExist) { + client.makeBucket(bucket); + } + InputStream inputStream = IoUtil.toUtf8Stream(content); + PutObjectOptions options = new PutObjectOptions(inputStream.available(), MagicNumConstant.NEGATIVE_ONE); + client.putObject(bucket, fullFilePath, inputStream, options); + } + + /** + * 读取文件 + * + * @param bucketName 桶 + * @param fullFilePath 文件存储的全路径,包括文件名,非'/'开头. e.g. dataset/12/annotation/test.txt + * @return String + */ + public String readString(String bucketName, String fullFilePath) throws Exception { + try (InputStream is = client.getObject(bucketName, fullFilePath)) { + return IoUtil.read(is, Charset.defaultCharset()); + } + } + + /** + * 文件删除 + * + * @param bucketName 桶 + * @param fullFilePath 文件存储的全路径,包括文件名,非'/'开头. e.g. dataset/12/annotation/test.txt + */ + public void del(String bucketName, String fullFilePath) throws Exception { + Iterable> items = client.listObjects(bucketName, fullFilePath); + Set files = new HashSet<>(); + for (Result item : items) { + files.add(item.get().objectName()); + } + Iterable> results = client.removeObjects(bucketName, files); + for (Result result : results) { + result.get(); + } + } + + /** + * 批量删除文件 + * + * @param bucketName 桶 + * @param objectNames 对象名称 + */ + public void delFiles(String bucketName, List objectNames) throws Exception { + Iterable> results = client.removeObjects(bucketName, objectNames); + for (Result result : results) { + result.get(); + } + } + + /** + * 获取对象名称 + * + * @param bucketName 桶名称 + * @param prefix 前缀 + * @return List 对象名称列表 + * @throws Exception + */ + public List getObjects(String bucketName, String prefix) throws Exception { + List fileNames = new ArrayList<>(); + Iterable> results = client.listObjects(bucketName, prefix); + for (Result result : results) { + Item item = result.get(); + fileNames.add(item.objectName()); + } + return fileNames; + } + + /** + * 获取路径下文件数量 + * + * @param bucketName 桶名称 + * @param prefix 前缀 + * @return InputStream 文件流 + * @throws Exception + */ + public int getCount(String bucketName, String prefix) throws Exception { + int count = NumberConstant.NUMBER_0; + Iterable> results = client.listObjects(bucketName, prefix); + for (Result result : results) { + count++; + } + return count; + } + + /** + * 获取文件流 + * + * @param bucketName 桶 + * @param objectName 对象名称 + * @return InputStream 文件流 + * @throws Exception + */ + public InputStream getObjectInputStream(String bucketName, String objectName) throws Exception { + return client.getObject(bucketName, objectName); + } + + /** + * 文件夹复制 + * + * @param bucketName 桶 + * @param sourceFiles 源文件 + * @param targetDir 目标文件夹 + */ + public void copyDir(String bucketName, List sourceFiles, String targetDir) { + sourceFiles.forEach(sourceFile -> { + InputStream inputStream = null; + try { + String sourceObjectName = sourceFile; + String targetObjectName = targetDir + "/" + StringUtils.substringAfterLast(sourceObjectName, "/"); + inputStream = client.getObject(bucketName, sourceObjectName); + byte[] buf = new byte[512]; + int bytesRead; + int count = MagicNumConstant.ZERO; + while ((bytesRead = inputStream.read(buf, MagicNumConstant.ZERO, buf.length)) >= MagicNumConstant.ZERO) { + count += bytesRead; + } + PutObjectOptions options = new PutObjectOptions(count, MagicNumConstant.ZERO); + client.putObject(bucketName, targetObjectName, client.getObject(bucketName, sourceObjectName), options); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "MinIO file copy exception, {}", e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + LogUtil.error(LogEnum.BIZ_DATASET, "MinIO file read stream closed failed, {}", e); + } + } + }); + } + + /** + * minio拷贝操作 + * + * @param bucketName 桶名 + * @param sourceFiles 需要复制的标注文件名 + * @param targetDir 目标文件夹路径 + */ + public void copyObject(String bucketName, List sourceFiles, String targetDir) { + CopyConditions copyConditions = new CopyConditions(); + sourceFiles.forEach(sourceFile -> { + try { + String targetName = targetDir + "/" + StringUtils.substringAfterLast(sourceFile, "/"); + client.copyObject(bucketName, targetName, null, null, bucketName, sourceFile, null, copyConditions); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "MinIO file copy failed, {}", e); + } + }); + } + + /** + * 获取文件列表 + * + * @param bucketName 桶 + * @param prefix 前缀 + * @param recursive 是否递归查询 + * @return List 文件列表 + */ + public List fileList(String bucketName, String prefix, boolean recursive) { + List result = new ArrayList<>(); + Iterable> items = client.listObjects(bucketName, prefix, false); + for (Result resultItem : items) { + try { + Item item = resultItem.get(); + FileDTO fileDto = FileDTO.builder().dir(item.isDir()).size(item.size()).path(item.objectName()) + .name(item.objectName().substring(item.objectName().lastIndexOf("/") + 1, item.objectName().length())) + .build(); + if(!item.isDir()) { + fileDto.setLastModified(Date.from(item.lastModified().toInstant())); + } + result.add(fileDto); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "get file list error {}", e); + } + } + return result; + } + + /** + * 生成一个给HTTP PUT请求用的presigned URL。浏览器/移动端的客户端可以用这个URL进行上传, + * 即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天 + * + * @param bucketName 存储桶名称 + * @param objectName 存储桶里的对象名称 + * @param expires 失效时间(以秒为单位),默认是7天,不得大于七天 + * @return String + */ + public String getEncryptedPutUrl(String bucketName, String objectName, Integer expires) { + if (StringUtils.isEmpty(objectName)) { + throw new BusinessException("object name cannot be empty"); + } + try { + return client.presignedPutObject(bucketName, objectName, expires); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, e.getMessage()); + throw new BusinessException("MinIO an error occurred, please contact the administrator"); + } + } + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioWebTokenBody.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioWebTokenBody.java new file mode 100644 index 0000000..7aab91e --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/MinioWebTokenBody.java @@ -0,0 +1,81 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.utils; + +import cn.hutool.http.HttpRequest; +import com.alibaba.fastjson.JSONObject; +import lombok.Data; +import org.apache.commons.lang.StringUtils; +import org.dubhe.biz.file.dto.MinioDownloadDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description Minio web访问token实体 + * @date 2020-05-09 + */ +@Data +@Service +public class MinioWebTokenBody { + + @Value("${minioweb.GetToken.url}") + private String tokenUrl; + @Value("${minioweb.GetToken.param.id}") + private int id; + @Value("${minioweb.GetToken.param.jsonrpc}") + private String jsonrpc; + @Value("${minioweb.GetToken.param.method}") + private String method; + @Value("${minioweb.zip.url}") + private String zipUrl; + @Value("${minio.accessKey}") + private String accessKey; + @Value("${minio.secretKey}") + private String secretKey; + @Value("${minio.url}") + private String url; + + /** + * 生成文件下载请求参数方法 + * + * @param bucketName 桶名称 + * @param prefix 前缀 + * @param objects 对象名称 + * @return MinioDownloadDto 下载请求参数 + */ + public MinioDownloadDTO getDownloadParam(String bucketName, String prefix, List objects, String zipName) { + String paramTemplate = "{\"id\":%d,\"jsonrpc\":\"%s\",\"params\":{\"username\":\"%s\",\"password\":\"%s\"},\"method\":\"%s\"}"; + String downloadBodyTemplate = "{\"bucketName\":\"%s\",\"prefix\":\"%s\",\"objects\":[%s]}"; + String param = String.format(paramTemplate, id, jsonrpc, accessKey, secretKey, method); + String result = HttpRequest.post(url + tokenUrl).contentType("application/json").body(param).execute().body(); + String token = JSONObject.parseObject(result).getJSONObject("result").getString("token"); + return new MinioDownloadDTO(token, String.format(downloadBodyTemplate, bucketName, prefix, getStrFromList(objects)), zipName); + } + + public String getStrFromList(List objects) { + List result = new ArrayList<>(); + objects.stream().forEach(s -> { + result.add("\"" + s + "\""); + }); + return StringUtils.join(result, ","); + } + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsFactory.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsFactory.java new file mode 100644 index 0000000..1b3a719 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsFactory.java @@ -0,0 +1,78 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.utils; + +import com.emc.ecs.nfsclient.nfs.nfs3.Nfs3; +import com.emc.ecs.nfsclient.rpc.CredentialUnix; +import org.apache.commons.pool2.PooledObject; +import org.apache.commons.pool2.PooledObjectFactory; +import org.apache.commons.pool2.impl.DefaultPooledObject; +import org.dubhe.biz.file.config.NfsConfig; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + * @description NFS工厂类 + * @date 2020-05-13 + */ +@Deprecated +@Component +public class NfsFactory implements PooledObjectFactory { + + private final NfsConfig nfsConfig; + + public NfsFactory(NfsConfig nfsConfig) { + this.nfsConfig = nfsConfig; + } + + @Override + public PooledObject makeObject() { + Nfs3 nfs3 = null; + try { + nfs3 = new Nfs3(nfsConfig.getNfsIp(), nfsConfig.getRootDir(), new CredentialUnix(0, 0, null), 3); + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "创建NFS对象失败: ", e); + } + return new DefaultPooledObject<>(nfs3); + } + + @Override + public void destroyObject(PooledObject pooledObject) { + LogUtil.info(LogEnum.NFS_UTIL, "销毁NFS对象: ", pooledObject.getObject()); + } + + @Override + public boolean validateObject(PooledObject pooledObject) { + LogUtil.info(LogEnum.NFS_UTIL, "验证NFS对象: ", pooledObject.getObject()); + return true; + } + + @Override + public void activateObject(PooledObject pooledObject) { + LogUtil.info(LogEnum.NFS_UTIL, "激活NFS对象: ", pooledObject.getObject()); + + } + + @Override + public void passivateObject(PooledObject pooledObject) { + LogUtil.info(LogEnum.NFS_UTIL, "钝化NFS对象: ", pooledObject.getObject()); + } +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsPool.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsPool.java new file mode 100644 index 0000000..f971b83 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsPool.java @@ -0,0 +1,116 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.utils; + +import com.emc.ecs.nfsclient.nfs.nfs3.Nfs3; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.stereotype.Component; + +/** + * @description NFS3 连接池 + * @date 2020-05-18 + */ +@Deprecated +@Component +public class NfsPool { + /** + * NFS工厂对象 + */ + private NfsFactory nfsFactory; + /** + * GenericObjectPool对象 + */ + private final GenericObjectPool genericObjectPool; + /** + * 最大总共连接数 + */ + public static final int MAX_TOTAL = 300; + /** + * 最小连接数 + */ + public static final int MIN = 20; + /** + * 最大连接数 + */ + public static final int MAX = 300; + /** + * 最大等待时间 单位毫秒 + */ + public static final int MAX_WAIT_TIME = 3000; + + /** + * 初始化连接池 + * + * @param nfsFactory + */ + public NfsPool(NfsFactory nfsFactory) { + this.nfsFactory = nfsFactory; + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); + poolConfig.setMaxTotal(MAX_TOTAL); + poolConfig.setMinIdle(MIN); + poolConfig.setMaxIdle(MAX); + poolConfig.setMaxWaitMillis(MAX_WAIT_TIME); + this.genericObjectPool = new GenericObjectPool<>(nfsFactory, poolConfig); + } + + /** + * 从连接池中取连接 + * + * @return nfs3 + */ + public Nfs3 getNfs() { + try { + LogUtil.info(LogEnum.NFS_UTIL,"NFS线程 活跃数量:{} ,空闲数量: {} , 等待队列数量 : {}",genericObjectPool.getNumActive(),genericObjectPool.getNumIdle(),genericObjectPool.getNumWaiters()); + return genericObjectPool.borrowObject(); + } catch (Exception e) { + LogUtil.error(LogEnum.NFS_UTIL, "获取NFS连接失败: {} ", e); + return null; + } + } + + /** + * 释放连接到连接池 + * + * @param nfs3 + */ + public void revertNfs(Nfs3 nfs3) { + try { + if(nfs3 != null){ + LogUtil.info(LogEnum.NFS_UTIL,"成功释放对象 : {} ", nfs3); + genericObjectPool.returnObject(nfs3); + } + } catch (Exception e) { + LogUtil.error(LogEnum.NFS_UTIL, " 释放NFS连接失败: ", e); + } + } + + /** + * 销毁公共池 + */ + public void destroyPool() { + try { + genericObjectPool.close(); + } catch (Exception e) { + LogUtil.error(LogEnum.NFS_UTIL, "销毁NFS连接失败: ", e); + } + } + +} diff --git a/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsUtil.java b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsUtil.java new file mode 100644 index 0000000..b4cf344 --- /dev/null +++ b/dubhe-server/common-biz/file/src/main/java/org/dubhe/biz/file/utils/NfsUtil.java @@ -0,0 +1,776 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.file.utils; + +import cn.hutool.core.util.StrUtil; +import com.emc.ecs.nfsclient.nfs.io.Nfs3File; +import com.emc.ecs.nfsclient.nfs.io.NfsFileInputStream; +import com.emc.ecs.nfsclient.nfs.io.NfsFileOutputStream; +import lombok.Getter; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.apache.commons.io.IOUtils; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.config.NfsConfig; +import org.dubhe.biz.file.exception.NfsBizException; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * @description NFS Util + * !!!注意: 使用NFSFile 对象 业务上自行需要关闭 NFS 对象 + * @date 2020-05-12 + */ +@Deprecated +@Component +@Getter +public class NfsUtil { + + private final NfsConfig nfsConfig; + + private static final String FILE_SEPARATOR = "/"; + + private static final String ZIP = ".zip"; + + private static final String CHARACTER_GBK = "GBK"; + + private static final String CHARACTER_UTF_8 = "UTF-8"; + + private static final String UNDER_LINE = "_"; + + @Autowired + private NfsPool nfsPool; + + + public NfsUtil(NfsConfig nfsConfig) { + this.nfsConfig = nfsConfig; + } + + private String getReplaceRootPathRegex() { + String rootPath = nfsConfig.getRootDir(); + return "^" + rootPath.substring(MagicNumConstant.ZERO, rootPath.length() - MagicNumConstant.ONE); + } + + /** + * 获取NFS3File 对象 + * + * @param path 文件路径 + * @return Nfs3File + */ + public Nfs3File getNfs3File(String path) { + if (StringUtils.isEmpty(formatPath(path))) { + LogUtil.error(LogEnum.NFS_UTIL, "传入的NFS3初始化路径 {} ,无法初始化NFS3 ", path); + throw new NfsBizException("初始化路径:" + path + " 不合法"); + } + Nfs3File nfs3File; + try { + nfs3File = new Nfs3File(nfsPool.getNfs(), path); + LogUtil.info(LogEnum.NFS_UTIL, "成功获取NFS3File对象 : {} ", nfs3File.getName()); + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "获取NFS3File对象失败 :", e); + throw new NfsBizException("未获取到NFS连接或者NFS连接池已满" + e.getMessage()); + } + return nfs3File; + } + + /** + * 获取NFS3File 对象文件的输入流 + * + * @param nfs3File + * @return BufferedInputStream + */ + public BufferedInputStream getInputStream(Nfs3File nfs3File) { + BufferedInputStream stream = null; + try { + if (!nfs3File.isFile()) { + throw new NfsBizException("此路径下查找到的对象不是文件类型,请检查文件类型是否正确!"); + } + stream = new BufferedInputStream(new NfsFileInputStream(nfs3File)); + } catch (IOException e) { + throw new NfsBizException("nfs获取对象输出流失败!"); + } + return stream; + } + + /** + * 获取NFS3File 对象文件的输入流 + * + * @param path 文件路径 + * @return BufferedInputStream + */ + public BufferedInputStream getInputStream(String path) { + Nfs3File nfs3File = getNfs3File(formatPath(path)); + if (nfs3File == null) { + throw new NfsBizException("此路径" + path + "下没有文件可以加载!"); + } + return getInputStream(nfs3File); + } + + /** + * 校验文件或文件夹是否不存在(true为不存在,false为存在) + * + * @param path 文件路径 + * @return boolean + */ + public boolean fileOrDirIsEmpty(String path) { + if (!StringUtils.isEmpty(path)) { + path = formatPath(path.startsWith(nfsConfig.getRootDir()) ? path.replaceFirst(nfsConfig.getRootDir(), StrUtil.SLASH) : path); + Nfs3File nfs3File = getNfs3File(path); + try { + if (nfs3File.exists()) { + return false; + } + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "判断NFS File异常: ", e); + return true; + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + } + return true; + + } + + + /** + * 创建指定NFS目录 + * + * @param dir 需要创建的目录 例如:/abc/def + * @return boolean + */ + public boolean createDir(String dir) { + if (!StringUtils.isEmpty(dir)) { + dir = formatPath(dir); + String[] paths = dir.substring(MagicNumConstant.ONE).split(FILE_SEPARATOR); + StringBuilder sbPath = new StringBuilder(); + for (String path : paths) { + sbPath.append(FILE_SEPARATOR).append(path); + Nfs3File nfs3File = getNfs3File(sbPath.toString()); + try { + if (!nfs3File.exists()) { + nfs3File.mkdirs(); + } + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "创建NFS目录失败:", e); + return false; + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + } + return true; + } + return false; + } + + /** + * 创建多个指定目录 + * + * @param removeNfsRootPath + * @param paths + * @return boolean + */ + private boolean createDir(boolean removeNfsRootPath, String... paths) { + for (String path : paths) { + if (path == null) { + continue; + } + String formatPath = path; + if (removeNfsRootPath) { + formatPath = formatPath.replaceAll(this.getReplaceRootPathRegex(), ""); + } + boolean res = createDir(formatPath); + if (!res) { + return false; + } + } + return true; + } + + + /** + * 创建指定NFS目录 + * + * @param paths 路径 例如:/nfs/abc/def/ /abc/def/ + * @param nfsRootPath 是否包含nfs根目录 true:包含 false:不包含 + * @return boolean + */ + public boolean createDirs(boolean nfsRootPath, String... paths) { + if (null == paths || paths.length < MagicNumConstant.ONE) { + return true; + } + for (String path : paths) { + if (path == null) { + continue; + } + String formatPath = path; + if (nfsRootPath) { + formatPath = formatPath.replaceAll(this.getReplaceRootPathRegex(), ""); + } + if (!createDir(formatPath)) { + return false; + } + } + return true; + } + + /** + * 指定目录NFS中创建文件 + * + * @param dir 需要创建的目录 例如:/abc/def + * @param fileName 需要创建的文件 例如:dd.txt + * @return boolean + */ + public boolean createFile(String dir, String fileName) { + if (!StringUtils.isEmpty(dir) && !StringUtils.isEmpty(fileName)) { + dir = formatPath(dir); + Nfs3File nfs3File = getNfs3File(dir + FILE_SEPARATOR + fileName); + try { + if (!nfs3File.exists()) { + nfs3File.mkdirs(); + } + nfs3File.createNewFile(); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "创建NFS文件失败: ", e); + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + } + return false; + } + + /** + * 删除NFS目录 或者文件 + * + * @param dirOrFile 需要删除的目录 或者文件 例如:/abc/def 或者 /abc/def/dd.txt + * @return boolean + */ + public boolean deleteDirOrFile(String dirOrFile) { + if (!StringUtils.isEmpty(dirOrFile)) { + dirOrFile = formatPath(dirOrFile); + try { + List nfs3FileList = getNfs3File(dirOrFile).listFiles(); + //删除目录下的子文件 + if (!CollectionUtils.isEmpty(nfs3FileList)) { + for (Nfs3File nfs3File : nfs3FileList) { + if (nfs3File.isDirectory()) { + deleteDirOrFile(nfs3File.getPath()); + } else if (nfs3File.isFile()) { + try { + nfs3File.delete(); + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + } + } + } + Nfs3File sourceNfsFile = getNfs3File(dirOrFile); + try { + sourceNfsFile.delete(); + } finally { + nfsPool.revertNfs(sourceNfsFile.getNfs()); + } + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.NFS_UTIL, "删除NFS目录失败:", e); + } + } + return false; + } + + + /** + * 上传文件到 NFS 指定目录 + * + * @param sourceFile 本地文件 包含路径 例如:/abc/def/gg.txt + * @param targetDir 指定目录 例如:/abc/def + * @return boolean + */ + public boolean uploadFileToNfs(String sourceFile, String targetDir) { + if (StringUtils.isEmpty(sourceFile) || StringUtils.isEmpty(targetDir)) { + return false; + } + sourceFile = formatPath(sourceFile); + targetDir = formatPath(targetDir); + //本地文件对象 + File localFile = new File(sourceFile); + Nfs3File nfs3File = getNfs3File(targetDir + FILE_SEPARATOR + localFile.getName()); + try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(localFile)); + BufferedOutputStream outputStream = new BufferedOutputStream(new NfsFileOutputStream(nfs3File))) { + IOUtils.copyLarge(inputStream, outputStream); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "上传失败: ", e); + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + return false; + } + + /** + * 下载NFS文件到本地目录 + * + * @param sourceFile 指定文件 例如:/abc/def/dd.txt + * @param targetPath 目标目录 例如: /abc/dd + * @return boolean + */ + public boolean downFileFormNfs(String sourceFile, String targetPath) { + if (StringUtils.isEmpty(sourceFile) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourceFile = formatPath(sourceFile); + targetPath = formatPath(targetPath); + Nfs3File nfsFile = getNfs3File(sourceFile); + if (nfsFile != null) { + try (InputStream inputStream = new BufferedInputStream(new NfsFileInputStream(nfsFile)); + OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(new File(targetPath + FILE_SEPARATOR + nfsFile.getName())))) { + IOUtils.copyLarge(inputStream, outputStream); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "下载失败:", e); + } finally { + nfsPool.revertNfs(nfsFile.getNfs()); + } + } + return false; + } + + + /** + * NFS 复制文件 到指定目录下 单个文件 + * + * @param sourceFile 需要复制的文件 例如:/abc/def/dd.txt + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + public boolean copyFile(String sourceFile, String targetPath) { + if (StringUtils.isEmpty(sourceFile) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourceFile = formatPath(sourceFile); + targetPath = formatPath(targetPath); + Nfs3File sourceNfsFile = null; + Nfs3File targetNfsFileNew = null; + try { + sourceNfsFile = getNfs3File(sourceFile); + targetNfsFileNew = getNfs3File(targetPath + FILE_SEPARATOR + sourceNfsFile.getName()); + if (!targetNfsFileNew.exists()) { + createDir(targetPath); + } + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "创建目标文件失败: ", e); + } + try (InputStream inputStream = new BufferedInputStream(new NfsFileInputStream(sourceNfsFile)); + OutputStream outputStream = new BufferedOutputStream(new NfsFileOutputStream(targetNfsFileNew))) { + targetNfsFileNew.createNewFile(); + IOUtils.copyLarge(inputStream, outputStream); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "复制失败:", e); + return false; + } finally { + nfsPool.revertNfs(sourceNfsFile.getNfs()); + nfsPool.revertNfs(targetNfsFileNew.getNfs()); + } + } + + + /** + * NFS 复制目录到指定目录下 多个文件 包含目录与文件并存情况 + * + * 通过NFS文件复制方式 可能存在NFS RPC协议超时情况 + * + * @param sourcePath 需要复制的文件目录 例如:/abc/def + * @param targetPath 需要放置的目标目录 例如:/abc/dd + * @return boolean + */ + public boolean copyNfsPath(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourcePath = formatPath(sourcePath); + targetPath = formatPath(targetPath); + try { + Nfs3File sourceNfsFile = getNfs3File(sourcePath); + List nfs3FileList = sourceNfsFile.listFiles(); + if (CollectionUtils.isEmpty(nfs3FileList)) { + createDir(targetPath + sourcePath.substring(sourcePath.lastIndexOf(FILE_SEPARATOR))); + } else { + for (Nfs3File nfs3File : nfs3FileList) { + if (nfs3File.isDirectory()) { + String newTargetPath = nfs3File.getPath().substring(nfs3File.getPath().lastIndexOf(FILE_SEPARATOR)); + Nfs3File newNfs3File = getNfs3File(newTargetPath); + try { + if (!newNfs3File.exists()) { + createDir(targetPath + newTargetPath); + } + copyNfsPath(nfs3File.getPath(), targetPath + newTargetPath); + } finally { + nfsPool.revertNfs(newNfs3File.getNfs()); + } + } + if (nfs3File.isFile()) { + copyFile(sourcePath + FILE_SEPARATOR + nfs3File.getName(), targetPath); + } + } + } + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "复制失败: ", e); + return false; + } + } + + /** + * zip解压并删除压缩文件 + * 当压缩包文件较多时,可能会因为RPC超时而解压失败 + * 该方法已废弃,请使用org.dubhe.utils.LocalFileUtil类的unzipLocalPath方法来替代 + * @param sourcePath zip源文件 例如:/abc/z.zip + * @param targetPath 解压后的目标文件夹 例如:/abc/ + * @return boolean + */ + @Deprecated + public boolean unzip(String sourcePath, String targetPath) { + if (StringUtils.isEmpty(sourcePath) || StringUtils.isEmpty(targetPath)) { + return false; + } + sourcePath = formatPath(sourcePath); + targetPath = formatPath(targetPath); + if (!sourcePath.toLowerCase().endsWith(ZIP)) { + return false; + } + ArchiveInputStream zIn = null; + Nfs3File sourceNfsFile = getNfs3File(sourcePath); + try { + zIn = new ZipArchiveInputStream(new BufferedInputStream(new NfsFileInputStream(sourceNfsFile)), CHARACTER_GBK, false, true); + //判断压缩文件编码方式,并重新获取NFS对象流 + try { + zIn.getNextEntry(); + zIn.close(); + zIn = new ZipArchiveInputStream(new BufferedInputStream(new NfsFileInputStream(sourceNfsFile)), CHARACTER_GBK, false, true); + } catch (Exception e) { + zIn.close(); + zIn = new ZipArchiveInputStream(new BufferedInputStream(new NfsFileInputStream(sourceNfsFile)), CHARACTER_UTF_8, false, true); + } + ZipEntry entry; + while ((entry = (ZipEntry) zIn.getNextEntry()) != null) { + if (entry.isDirectory()) { + createDir(targetPath + FILE_SEPARATOR + entry.getName()); + } else { + //若文件夹未创建则创建文件夹 + if (entry.getName().contains(FILE_SEPARATOR)) { + String entryName = entry.getName(); + String zipDirName = entryName.substring(MagicNumConstant.ZERO, entryName.lastIndexOf(FILE_SEPARATOR)); + createDir(targetPath + FILE_SEPARATOR + zipDirName); + } + Nfs3File nfs3File = getNfs3File(targetPath + FILE_SEPARATOR + entry.getName()); + try { + if (!nfs3File.exists()) { + nfs3File.createNewFile(); + } + BufferedOutputStream bos = new BufferedOutputStream(new NfsFileOutputStream(nfs3File)); + IOUtils.copyLarge(zIn, bos); + bos.flush(); + bos.close(); + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + } + } + sourceNfsFile.delete(); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "解压失败: ", e); + return false; + } finally { + nfsPool.revertNfs(sourceNfsFile.getNfs()); + if (zIn != null) { + try { + zIn.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "输入流关闭失败: ", e); + } + } + } + + } + + /** + * NFS 解压压缩包 包含目录与子目录 + * + * @param sourcePath 需要复制的文件 例如:/abc/def/aaa.rar + * @return boolean + */ + public boolean unZip(String sourcePath) { + sourcePath = formatPath(sourcePath); + if (StringUtils.isEmpty(sourcePath)) { + return false; + } + String fileDir = sourcePath.substring(MagicNumConstant.ZERO, sourcePath.lastIndexOf(FILE_SEPARATOR)); + ZipEntry zipEntry = null; + try (ZipInputStream zipInputStream = new ZipInputStream(new NfsFileInputStream(getNfs3File(sourcePath)))) { + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (zipEntry.isDirectory()) { + createDir(fileDir + FILE_SEPARATOR + zipEntry.getName()); + continue; + } + Nfs3File targetNfsFileNew = getNfs3File(fileDir + FILE_SEPARATOR + zipEntry.getName()); + try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new NfsFileOutputStream(targetNfsFileNew))) { + targetNfsFileNew.createNewFile(); + IOUtils.copyLarge(zipInputStream, bufferedOutputStream); + } finally { + nfsPool.revertNfs(targetNfsFileNew.getNfs()); + } + } + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "解压文件失败 : ", e); + return false; + } + } + + /** + * 压缩NFS 目录 或者文件 到压缩包 + * + * @param dirOrFile 目录或者文件 例如: /abc/def/aaa.txt , /abc/def + * @param zipName 压缩包名称 例如: aa,bb,cc + * @return boolean + */ + public boolean zipDirOrFile(String dirOrFile, String zipName) { + Nfs3File nfs3File = getNfs3File(formatPath(dirOrFile)); + try (ZipOutputStream zipOutputStream = getFileZipOutputStream(getNfsFilePath(formatPath(dirOrFile)), zipName)) { + zipFiles(zipOutputStream, nfs3File); + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "压缩文件失败 : ", e); + return false; + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + } + + /** + * 获取NFS 文件压缩目录 + * + * @param dirOrFile + * @return String + */ + private String getNfsFilePath(String dirOrFile) throws IOException { + Nfs3File nfs3File = getNfs3File(formatPath(dirOrFile)); + if (nfs3File.isFile()) { + nfsPool.revertNfs(nfs3File.getNfs()); + return dirOrFile.substring(MagicNumConstant.ZERO, dirOrFile.lastIndexOf(FILE_SEPARATOR)); + } + return dirOrFile; + } + + /** + * 根据文件路劲 获取Zip文件流 + * + * @param dirOrFile + * @param zipName + * @return ZipOutputStream + */ + private ZipOutputStream getFileZipOutputStream(String dirOrFile, String zipName) throws IOException { + Nfs3File targetNfsFileNew = getNfs3File(getNfsFilePath(formatPath(dirOrFile)) + FILE_SEPARATOR + zipName + ZIP); + targetNfsFileNew.createNewFile(); + return new ZipOutputStream(new NfsFileOutputStream(targetNfsFileNew)); + } + + /** + * 压缩文件和文件夹 + * + * @param zipOutputStream + * @param nfs3File + * @return boolean + */ + public boolean zipFiles(ZipOutputStream zipOutputStream, Nfs3File nfs3File) { + try { + if (nfs3File.isFile()) { + compressZip(zipOutputStream, nfs3File, ""); + } else { + List nfs3FileList = nfs3File.listFiles(); + if (!CollectionUtils.isEmpty(nfs3FileList)) { + for (Nfs3File nfs3FileChildren : nfs3FileList) { + zipFiles(zipOutputStream, nfs3FileChildren); + } + } + } + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "压缩文件失败 : ", e); + return false; + } + } + + /** + * 单个文件压缩 + * + * @param zipOutputStream + * @param nfs3File + */ + public void compressZip(ZipOutputStream zipOutputStream, Nfs3File nfs3File, String childPath) { + try (InputStream inputStream = new BufferedInputStream(new NfsFileInputStream(nfs3File))) { + if (StringUtils.isEmpty(childPath)) { + zipOutputStream.putNextEntry(new ZipEntry(nfs3File.getName())); + } else { + zipOutputStream.putNextEntry(new ZipEntry(childPath + FILE_SEPARATOR + nfs3File.getName())); + } + byte[] buffer = new byte[1024 * 10]; + int length; + while ((length = inputStream.read(buffer, MagicNumConstant.ZERO, buffer.length)) != -1) { + zipOutputStream.write(buffer, MagicNumConstant.ZERO, length); + } + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "解压单个文件异常: ", e); + } finally { + nfsPool.revertNfs(nfs3File.getNfs()); + } + } + + /** + * 复制文件夹(或文件)到另一个文件夹 + * + * @param sourcePath 复制文件夹 /abc/def 复制文件:/abc/def/dd.txt + * @param targetPath /abc/dd/def + * @return boolean + */ + public boolean copyDirs(String sourcePath, String targetPath) { + Nfs3File sourceNfsFile = getNfs3File(formatPath(sourcePath)); + try { + if (!sourceNfsFile.exists()) { + LogUtil.error(LogEnum.NFS_UTIL, "sourcePath不存在, 如下{} ", sourcePath); + return false; + } + if (sourceNfsFile.isFile()) { + return copyFile(sourcePath, targetPath); + } else if (sourceNfsFile.isDirectory()) { + targetPath = targetPath + FILE_SEPARATOR + sourceNfsFile.getName(); + boolean bool = createDir(formatPath(targetPath)); + if (!bool) { + LogUtil.error(LogEnum.NFS_UTIL, "{}文件夹创建失败... ", targetPath); + return false; + } + List files = sourceNfsFile.listFiles(); + for (Nfs3File file : files) { + copyDirs(file.getPath(), targetPath); + } + } + return true; + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "copyDirs失败, sourcePath为 , targetPath为 , 失败原因 ", sourcePath, targetPath, e); + } finally { + nfsPool.revertNfs(sourceNfsFile.getNfs()); + } + return false; + } + + /** + * 找到倒数第二新的文件夹 + * + * @param parentPath 父文件夹 + * @return + */ + public String find2ndNewDir(String parentPath) { + Nfs3File parentNfsFile = getNfs3File(formatPath(parentPath)); + try { + if (!parentNfsFile.exists() || parentNfsFile.isFile()) { + LogUtil.error(LogEnum.NFS_UTIL, "sourcePath不存在, 如下{} ", parentPath); + return ""; + } + List files = parentNfsFile.listFiles(); + List dirs = new ArrayList<>(); + for (Nfs3File file : files) { + if (file.isDirectory()) { + dirs.add(file); + } + } + if (dirs.size() < MagicNumConstant.TWO) { + return ""; + } + dirs.sort((o1, o2) -> { + try { + return (int) (o2.lastModified() - o1.lastModified()); + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "执行异常: {} ", e); + return MagicNumConstant.ZERO; + } + }); + return dirs.get(MagicNumConstant.ONE).getName(); + + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "findSecNewDir失败, parentPath为{}, 失败原因{}", parentPath, e); + } finally { + nfsPool.revertNfs(parentNfsFile.getNfs()); + } + return ""; + } + + /** + * 重命名文件夹 + * + * @param sourcePath 原文件夹 + * @param targetPath 目标文件夹 + * @return + */ + public void renameDir(String sourcePath, String targetPath) { + Nfs3File sourceNfsFile = getNfs3File(formatPath(sourcePath)); + Nfs3File targetNfsFile = getNfs3File(formatPath(targetPath)); + try { + if (!sourceNfsFile.exists()) { + LogUtil.error(LogEnum.NFS_UTIL, "sourcePath不存在, 如下{} ", sourcePath); + } + sourceNfsFile.rename(targetNfsFile); + } catch (IOException e) { + LogUtil.error(LogEnum.NFS_UTIL, "renameDir失败, sourcePath为{}, targetPath为{}, 失败原因{}", sourcePath, targetPath, e); + } finally { + nfsPool.revertNfs(sourceNfsFile.getNfs()); + nfsPool.revertNfs(targetNfsFile.getNfs()); + } + } + + + /** + * 替换路劲中多余的 "/" + * + * @param path 文件路径 + * @return String + */ + public String formatPath(String path) { + if (!StringUtils.isEmpty(path)) { + return path.replaceAll("///*", FILE_SEPARATOR); + } + return path; + } + + public String getAbsolutePath(String relativePath) { + return nfsConfig.getRootDir() + nfsConfig.getBucket() + relativePath; + } + +} diff --git a/dubhe-server/common-biz/log/pom.xml b/dubhe-server/common-biz/log/pom.xml new file mode 100644 index 0000000..f049317 --- /dev/null +++ b/dubhe-server/common-biz/log/pom.xml @@ -0,0 +1,46 @@ + + + + common-biz + org.dubhe.biz + 0.0.1-SNAPSHOT + + 4.0.0 + + log + 0.0.1-SNAPSHOT + Biz log 工具 + Log for Dubhe Server + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + org.springframework + spring-web + + + javax.servlet + javax.servlet-api + ${javax.servlet-api.version} + + + + + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/aspect/LogAspect.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/aspect/LogAspect.java new file mode 100644 index 0000000..e01d0aa --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/aspect/LogAspect.java @@ -0,0 +1,81 @@ +/** + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dubhe.biz.log.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.UUID; + +/** + * @description 日志切面 + * @date 2020-04-10 + */ +@Component +@Aspect +@Slf4j +public class LogAspect { + + public static final String TRACE_ID = "traceId"; + + @Pointcut("execution(* org.dubhe..task..*.*(..)))") + public void serviceAspect() { + } + + @Pointcut("execution(* org.dubhe..rest..*.*(..))) ") + public void restAspect() { + } + + @Pointcut(" serviceAspect() ") + public void aroundAspect() { + } + + @Around("aroundAspect()") + public Object around(JoinPoint joinPoint) throws Throwable { + if (StringUtils.isEmpty(MDC.get(TRACE_ID))) { + MDC.put(TRACE_ID, UUID.randomUUID().toString()); + } + return ((ProceedingJoinPoint) joinPoint).proceed(); + } + + @Around("restAspect()") + public Object aroundRest(JoinPoint joinPoint) throws Throwable { + MDC.clear(); + MDC.put(TRACE_ID, UUID.randomUUID().toString()); + return combineLogInfo(joinPoint); + } + + private Object combineLogInfo(JoinPoint joinPoint) throws Throwable { + Object[] param = joinPoint.getArgs(); + LogUtil.info(LogEnum.LOG_ASPECT, "uri:{},input:{},==>begin", joinPoint.getSignature(), param); + long start = System.currentTimeMillis(); + Object result = ((ProceedingJoinPoint) joinPoint).proceed(); + long end = System.currentTimeMillis(); + LogUtil.info(LogEnum.LOG_ASPECT, "uri:{},output:{},proc_time:{}ms,<==end", joinPoint.getSignature().toString(), + result, end - start); + return result; + } + +} diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/entity/LogInfo.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/entity/LogInfo.java new file mode 100644 index 0000000..940f168 --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/entity/LogInfo.java @@ -0,0 +1,59 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.log.entity; + +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; + +import java.io.Serializable; + +/** + * @description 日志对象封装类 + * @date 2020-06-29 + */ +@Data +@Accessors(chain = true) +public class LogInfo implements Serializable { + + private static final long serialVersionUID = 5250395474667395607L; + + @JSONField(ordinal = MagicNumConstant.ONE) + private String traceId; + + @JSONField(ordinal = MagicNumConstant.TWO) + private String type; + + @JSONField(ordinal = MagicNumConstant.THREE) + private String level; + + @JSONField(ordinal = MagicNumConstant.FOUR) + private String location; + + @JSONField(ordinal = MagicNumConstant.FIVE) + private String time = DateUtil.now(); + + @JSONField(ordinal = MagicNumConstant.SIX) + private Object info; + + public void setInfo(Object info) { + this.info = info; + } +} diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/enums/LogEnum.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/enums/LogEnum.java new file mode 100644 index 0000000..0a09a89 --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/enums/LogEnum.java @@ -0,0 +1,95 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.log.enums; + +import lombok.Getter; + +/** + * @description 日志类型枚举类 + * @date 2020-06-23 + */ +@Getter +public enum LogEnum { + + // 系统报错日志 + SYS_ERR, + // 用户请求日志 + REST_REQ, + //全局请求日志 + GLOBAL_REQ, + // 训练模块 + BIZ_TRAIN, + //算法管理模块 + BIZ_ALGORITHM, + // 系统模块 + BIZ_SYS, + // 模型模块 + BIZ_MODEL, + // 模型优化 + MODEL_OPT, + // 数据集模块 + BIZ_DATASET, + // k8s模块 + BIZ_K8S, + //note book + NOTE_BOOK, + //NFS UTILS + NFS_UTIL, + //localFileUtil + LOCAL_FILE_UTIL, + //FILE UTILS + FILE_UTIL, + //FILE UTILS + UPLOAD_TEMP, + //STATE MACHINE + STATE_MACHINE, + //全局垃圾回收 + GARBAGE_RECYCLE, + //DATA_SEQUENCE + DATA_SEQUENCE, + //IO UTIL + IO_UTIL, + // 日志切面 + LOG_ASPECT, + // 远程调用 + REMOTE_CALL, + // 网关 + GATEWAY, + // Redis + REDIS, + //镜像 + IMAGE, + //度量 + MEASURE, + //云端Serving + SERVING; + + /** + * 判断日志类型不能为空 + * + * @param logType 日志类型 + * @return boolean 返回类型 + */ + public static boolean isLogType(LogEnum logType) { + + if (logType != null) { + return true; + } + return false; + } +} diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/BaseLogFilter.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/BaseLogFilter.java new file mode 100644 index 0000000..1ea3c1c --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/BaseLogFilter.java @@ -0,0 +1,79 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.log.filter; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.AbstractMatcherFilter; +import ch.qos.logback.core.spi.FilterReply; +import cn.hutool.core.util.StrUtil; +import org.slf4j.Marker; + +/** + * @description 自定义日志过滤器 + * @date 2020-07-21 + */ +public class BaseLogFilter extends AbstractMatcherFilter { + + Level level; + + /** + * 重写decide方法 + * + * @param iLoggingEvent event to decide upon. + * @return FilterReply + */ + @Override + public FilterReply decide(ILoggingEvent iLoggingEvent) { + if (!isStarted()) { + return FilterReply.NEUTRAL; + } + final String msg = iLoggingEvent.getMessage(); + //自定义级别 + if (checkLevel(iLoggingEvent) && msg != null && msg.startsWith(StrUtil.DELIM_START) && msg.endsWith(StrUtil.DELIM_END)) { + final Marker marker = iLoggingEvent.getMarker(); + if (marker != null && this.getName() != null && this.getName().contains(marker.getName())) { + return onMatch; + } + } + + return onMismatch; + } + + /** + * 检测日志级别 + * @param iLoggingEvent 日志事件 + * @return true 过滤当前级别 false 不过滤当前级别 + */ + protected boolean checkLevel(ILoggingEvent iLoggingEvent) { + return this.level != null + && iLoggingEvent.getLevel() != null + && iLoggingEvent.getLevel().toInt() == this.level.toInt(); + } + + public void setLevel(Level level) { + this.level = level; + } + + @Override + public void start() { + if (this.level != null) { + super.start(); + } + } +} \ No newline at end of file diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/ConsoleLogFilter.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/ConsoleLogFilter.java new file mode 100644 index 0000000..236dbcb --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/ConsoleLogFilter.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.log.filter; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.spi.FilterReply; +import org.dubhe.biz.log.utils.LogUtil; +import org.slf4j.MarkerFactory; + +/** + * @description 自定义日志过滤器 + * @date 2020-07-21 + */ +public class ConsoleLogFilter extends BaseLogFilter { + + @Override + public FilterReply decide(ILoggingEvent iLoggingEvent) { + if (!isStarted()) { + return FilterReply.NEUTRAL; + } + return checkLevel(iLoggingEvent) ? onMatch : onMismatch; + } + + @Override + protected boolean checkLevel(ILoggingEvent iLoggingEvent) { + + + return this.level != null + && iLoggingEvent.getLevel() != null + && iLoggingEvent.getLevel().toInt() >= this.level.toInt() + && !MarkerFactory.getMarker(LogUtil.K8S_CALLBACK_LEVEL).equals(iLoggingEvent.getMarker()) + && !MarkerFactory.getMarker(LogUtil.SCHEDULE_LEVEL).equals(iLoggingEvent.getMarker()) + && !"log4jdbc.log4j2".equals(iLoggingEvent.getLoggerName()); + } +} \ No newline at end of file diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/GlobalRequestLogFilter.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/GlobalRequestLogFilter.java new file mode 100644 index 0000000..8d42747 --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/filter/GlobalRequestLogFilter.java @@ -0,0 +1,34 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.log.filter; + +import ch.qos.logback.classic.spi.ILoggingEvent; + +/** + * @description 全局请求 日志过滤器 + * @date 2020-08-13 + */ +public class GlobalRequestLogFilter extends BaseLogFilter { + + + @Override + public boolean checkLevel(ILoggingEvent iLoggingEvent) { + return this.level != null; + } + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/handler/ScheduleTaskHandler.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/handler/ScheduleTaskHandler.java new file mode 100644 index 0000000..0f12247 --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/handler/ScheduleTaskHandler.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.log.handler; + + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; + +/** + * @description 定时任务处理器, 主要做日志标识 + * @date 2020-08-13 + */ +public class ScheduleTaskHandler { + + + public static void process(Handler handler) { + LogUtil.startScheduleTrace(); + try { + handler.run(); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_SYS, "There is something wrong in schedule task handler :{}", e); + } finally { + LogUtil.cleanTrace(); + } + } + + + public interface Handler { + void run(); + } +} diff --git a/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/utils/LogUtil.java b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/utils/LogUtil.java new file mode 100644 index 0000000..8a4f6f3 --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/java/org/dubhe/biz/log/utils/LogUtil.java @@ -0,0 +1,321 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.log.utils; + +import ch.qos.logback.classic.Level; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.aspect.LogAspect; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.log.entity.LogInfo; +import org.dubhe.biz.log.enums.LogEnum; +import org.slf4j.MDC; +import org.slf4j.MarkerFactory; +import org.slf4j.helpers.MessageFormatter; + +import java.util.Arrays; +import java.util.UUID; + +/** + * @description 日志工具类 + * @date 2020-06-29 + */ +@Slf4j +public class LogUtil { + + private static final String TRACE_TYPE = "TRACE_TYPE"; + + public static final String SCHEDULE_LEVEL = "SCHEDULE"; + + public static final String K8S_CALLBACK_LEVEL = "K8S_CALLBACK"; + + private static final String GLOBAL_REQUEST_LEVEL = "GLOBAL_REQUEST"; + + private static final String TRACE_LEVEL = "TRACE"; + + private static final String DEBUG_LEVEL = "DEBUG"; + + private static final String INFO_LEVEL = "INFO"; + + private static final String WARN_LEVEL = "WARN"; + + private static final String ERROR_LEVEL = "ERROR"; + + + public static void startScheduleTrace() { + MDC.put(TRACE_TYPE, SCHEDULE_LEVEL); + } + + public static void startK8sCallbackTrace() { + MDC.put(TRACE_TYPE, K8S_CALLBACK_LEVEL); + } + + public static void cleanTrace() { + MDC.clear(); + } + + /** + * info级别的日志 + * + * @param logType 日志类型 + * @param object 打印的日志参数 + * @return void + */ + + public static void info(LogEnum logType, Object... object) { + + logHandle(logType, Level.INFO, object); + } + + /** + * debug级别的日志 + * + * @param logType 日志类型 + * @param object 打印的日志参数 + * @return void + */ + public static void debug(LogEnum logType, Object... object) { + logHandle(logType, Level.DEBUG, object); + } + + /** + * error级别的日志 + * + * @param logType 日志类型 + * @param object 打印的日志参数 + * @return void + */ + public static void error(LogEnum logType, Object... object) { + errorObjectHandle(object); + logHandle(logType, Level.ERROR, object); + } + + /** + * warn级别的日志 + * + * @param logType 日志类型 + * @param object 打印的日志参数 + * @return void + */ + public static void warn(LogEnum logType, Object... object) { + logHandle(logType, Level.WARN, object); + } + + /** + * trace级别的日志 + * + * @param logType 日志类型 + * @param object 打印的日志参数 + * @return void + */ + public static void trace(LogEnum logType, Object... object) { + logHandle(logType, Level.TRACE, object); + } + + /** + * 日志处理 + * + * @param logType 日志类型 + * @param level 日志级别 + * @param object 打印的日志参数 + * @return void + */ + private static void logHandle(LogEnum logType, Level level, Object[] object) { + + LogInfo logInfo = generateLogInfo(logType, level, object); + + switch (logInfo.getLevel()) { + case TRACE_LEVEL: + log.trace(MarkerFactory.getMarker(TRACE_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + case DEBUG_LEVEL: + log.debug(MarkerFactory.getMarker(DEBUG_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + case GLOBAL_REQUEST_LEVEL: + logInfo.setLevel(null); + logInfo.setType(null); + logInfo.setLocation(null); + log.info(MarkerFactory.getMarker(GLOBAL_REQUEST_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + case SCHEDULE_LEVEL: + log.info(MarkerFactory.getMarker(SCHEDULE_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + case K8S_CALLBACK_LEVEL: + log.info(MarkerFactory.getMarker(K8S_CALLBACK_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + case INFO_LEVEL: + log.info(MarkerFactory.getMarker(INFO_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + case WARN_LEVEL: + log.warn(MarkerFactory.getMarker(WARN_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + case ERROR_LEVEL: + log.error(MarkerFactory.getMarker(ERROR_LEVEL), logJsonStringLengthLimit(logInfo)); + break; + default: + } + + } + + + /** + * 日志信息组装的内部方法 + * + * @param logType 日志类型 + * @param level 日志级别 + * @param object 打印的日志参数 + * @return LogInfo + */ + private static LogInfo generateLogInfo(LogEnum logType, Level level, Object[] object) { + + + LogInfo logInfo = new LogInfo(); + // 日志类型检测 + if (!LogEnum.isLogType(logType)) { + level = Level.ERROR; + object = new Object[MagicNumConstant.ONE]; + object[MagicNumConstant.ZERO] = "日志类型【".concat(String.valueOf(logType)).concat("】不正确!"); + logType = LogEnum.SYS_ERR; + } + + // 获取trace_id + if (StringUtils.isEmpty(MDC.get(LogAspect.TRACE_ID))) { + MDC.put(LogAspect.TRACE_ID, UUID.randomUUID().toString()); + } + // 设置logInfo的level,type,traceId属性 + logInfo.setLevel(level.levelStr) + .setType(logType.toString()) + .setTraceId(MDC.get(LogAspect.TRACE_ID)); + + + //自定义日志级别 + //LogEnum、 MDC中的 TRACE_TYPE 做日志分流标识 + if (Level.INFO.toInt() == level.toInt()) { + if (LogEnum.GLOBAL_REQ.equals(logType)) { + //info全局请求 + logInfo.setLevel(GLOBAL_REQUEST_LEVEL); + } else if (LogEnum.BIZ_K8S.equals(logType)) { + logInfo.setLevel(K8S_CALLBACK_LEVEL); + } else { + //schedule定时等 链路记录 + String traceType = MDC.get(TRACE_TYPE); + if (StringUtils.isNotBlank(traceType)) { + logInfo.setLevel(traceType); + } + } + } + + // 设置logInfo的堆栈信息 + setLogStackInfo(logInfo); + // 设置logInfo的info信息 + setLogInfo(logInfo, object); + // 截取logInfo的长度并转换成json字符串 + return logInfo; + } + + /** + * 设置loginfo的堆栈信息 + * + * @param logInfo 日志对象 + * @return void + */ + private static void setLogStackInfo(LogInfo logInfo) { + StackTraceElement[] elements = Thread.currentThread().getStackTrace(); + if (elements.length >= MagicNumConstant.SIX) { + StackTraceElement element = elements[MagicNumConstant.FIVE]; + logInfo.setLocation(String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber())); + } + } + + /** + * 限制log日志的长度并转换成json + * + * @param logInfo 日志对象 + * @return String + */ + private static String logJsonStringLengthLimit(LogInfo logInfo) { + try { + + String jsonString = JSON.toJSONString(logInfo); + if (StringUtils.isBlank(jsonString)) { + return ""; + } + if (jsonString.length() > MagicNumConstant.TEN_THOUSAND) { + String trunk = logInfo.getInfo().toString().substring(MagicNumConstant.ZERO, MagicNumConstant.NINE_THOUSAND); + logInfo.setInfo(trunk); + jsonString = JSON.toJSONString(logInfo); + } + return jsonString; + + } catch (Exception e) { + logInfo.setLevel(Level.ERROR.levelStr).setType(LogEnum.SYS_ERR.toString()) + .setInfo("cannot serialize exception: " + ExceptionUtils.getStackTrace(e)); + return JSON.toJSONString(logInfo); + } + } + + /** + * 设置日志对象的info信息 + * + * @param logInfo 日志对象 + * @param object 打印的日志参数 + * @return void + */ + private static void setLogInfo(LogInfo logInfo, Object[] object) { + + if (object.length > MagicNumConstant.ONE) { + logInfo.setInfo(MessageFormatter.arrayFormat(object[MagicNumConstant.ZERO].toString(), + Arrays.copyOfRange(object, MagicNumConstant.ONE, object.length)).getMessage()); + + } else if (object.length == MagicNumConstant.ONE && object[MagicNumConstant.ZERO] instanceof Exception) { + logInfo.setInfo((ExceptionUtils.getStackTrace((Exception) object[MagicNumConstant.ZERO]))); + log.error((ExceptionUtils.getStackTrace((Exception) object[MagicNumConstant.ZERO]))); + } else if (object.length == MagicNumConstant.ONE) { + logInfo.setInfo(object[MagicNumConstant.ZERO] == null ? "" : object[MagicNumConstant.ZERO]); + } else { + logInfo.setInfo(""); + } + + } + + /** + * 处理Exception的情况 + * + * @param object 打印的日志参数 + * @return void + */ + private static void errorObjectHandle(Object[] object) { + + if (object.length == MagicNumConstant.TWO && object[MagicNumConstant.ONE] instanceof Exception) { + log.error(String.valueOf(object[MagicNumConstant.ZERO]), (Exception) object[MagicNumConstant.ONE]); + object[MagicNumConstant.ONE] = ExceptionUtils.getStackTrace((Exception) object[MagicNumConstant.ONE]); + + } else if (object.length >= MagicNumConstant.THREE) { + log.error(String.valueOf(object[MagicNumConstant.ZERO]), + Arrays.copyOfRange(object, MagicNumConstant.ONE, object.length)); + for (int i = 0; i < object.length; i++) { + if (object[i] instanceof Exception) { + object[i] = ExceptionUtils.getStackTrace((Exception) object[i]); + } + + } + } + } +} diff --git a/dubhe-server/common-biz/log/src/main/resources/logback.xml b/dubhe-server/common-biz/log/src/main/resources/logback.xml new file mode 100644 index 0000000..efb061f --- /dev/null +++ b/dubhe-server/common-biz/log/src/main/resources/logback.xml @@ -0,0 +1,262 @@ + + + + + + + + + + + + ${log.pattern} + ${log.charset} + + + INFO + INFO + ACCEPT + DENY + + + + + + + logs/${log.path}/info/dubhe-info.log + + logs/${log.path}/info/dubhe-${app.active}-info-%d{yyyy-MM-dd}.%i.log + + + 50MB + 7 + 250MB + + + %m%n + ${log.charset} + + + true + + INFO + INFO,K8S_CALLBACK + ACCEPT + DENY + + + + + + logs/${log.path}/debug/dubhe-debug.log + + logs/${log.path}/debug/dubhe-${app.active}-debug-%d{yyyy-MM-dd}.%i.log + + + 50MB + 7 + 250MB + + + %m%n + ${log.charset} + + + true + + DEBUG + DEBUG + ACCEPT + DENY + + + + + + logs/${log.path}/error/dubhe-error.log + + logs/${log.path}/error/dubhe-${app.active}-error-%d{yyyy-MM-dd}.%i.log + + + 50MB + 7 + 250MB + + + %m%n + ${log.charset} + + + true + + ERROR + ERROR + ACCEPT + DENY + + + + + + logs/${log.path}/warn/dubhe-warn.log + + logs/${log.path}/warn/dubhe-${app.active}-warn-%d{yyyy-MM-dd}.%i.log + + + 50MB + 7 + 250MB + + + %m%n + ${log.charset} + + + true + + + WARN + WARN + ACCEPT + DENY + + + + + + logs/${log.path}/trace/dubhe-trace.log + + logs/${log.path}/trace/dubhe-${app.active}-trace-%d{yyyy-MM-dd}.%i.log + + + 50MB + 7 + 250MB + + + %m%n + ${log.charset} + + + true + + TRACE + TRACE + ACCEPT + DENY + + + + + + + logs/${log.path}/info/dubhe-schedule.log + + logs/${log.path}/info/dubhe-${app.active}-schedule-%d{yyyy-MM-dd}.%i.log + + + 50MB + 7 + 250MB + + + %m%n + ${log.charset} + + + true + + INFO + SCHEDULE + ACCEPT + DENY + + + + + + logs/${log.path}/info/dubhe-request.log + + logs/${log.path}/info/dubhe-${app.active}-request-%d{yyyy-MM-dd}.%i.log + + + 50MB + 7 + 250MB + + + %m%n + ${log.charset} + + + true + + INFO + + GLOBAL_REQUEST + ACCEPT + DENY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-biz/pom.xml b/dubhe-server/common-biz/pom.xml new file mode 100644 index 0000000..d8a2c9b --- /dev/null +++ b/dubhe-server/common-biz/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + pom + + org.dubhe + server + 0.0.1-SNAPSHOT + + org.dubhe.biz + common-biz + 0.0.1-SNAPSHOT + 通用Business模块(Biz) + Dubhe Common for Business + + + base + data-response + file + db + log + data-permission + redis + state-machine + + + + + + + + + + + + + diff --git a/dubhe-server/common-biz/redis/pom.xml b/dubhe-server/common-biz/redis/pom.xml new file mode 100644 index 0000000..60960da --- /dev/null +++ b/dubhe-server/common-biz/redis/pom.xml @@ -0,0 +1,34 @@ + + + + common-biz + org.dubhe.biz + 0.0.1-SNAPSHOT + + 4.0.0 + + redis + 0.0.1-SNAPSHOT + Biz redis 工具 + Redis for Dubhe Server + + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + com.liferay + com.fasterxml.jackson.databind + + + + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/config/RedisConfig.java b/dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/config/RedisConfig.java new file mode 100644 index 0000000..781b4f7 --- /dev/null +++ b/dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/config/RedisConfig.java @@ -0,0 +1,215 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.redis.config; + +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.ParserConfig; +import com.alibaba.fastjson.serializer.SerializerFeature; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.dubhe.biz.base.utils.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.Cache; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +/** + * @description redis配置类 + * @date 2020-03-25 + */ +@Slf4j +@Configuration +@EnableCaching +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +public class RedisConfig extends CachingConfigurerSupport { + + /** + * 设置 redis 数据默认过期时间,默认2小时 + * 设置@cacheable 序列化方式 + */ + @Bean + public RedisCacheConfiguration redisCacheConfiguration() { + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); + RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); + configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(2)); + return configuration; + } + + @SuppressWarnings("all") + @Bean(name = "redisTemplate") + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + //序列化 + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); + // value值的序列化采用fastJsonRedisSerializer + template.setValueSerializer(fastJsonRedisSerializer); + template.setHashValueSerializer(fastJsonRedisSerializer); + // 全局开启AutoType,这里方便开发,使用全局的方式 + ParserConfig.getGlobalInstance().setAutoTypeSupport(true); + // 建议使用这种方式,小范围指定白名单 + // ParserConfig.getGlobalInstance().addAccept("org.dubhe.domain"); + // key的序列化采用StringRedisSerializer + template.setKeySerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + + /** + * 自定义缓存key生成策略,默认将使用该策略 + */ + @Bean + @Override + public KeyGenerator keyGenerator() { + return (target, method, params) -> { + Map container = new HashMap<>(3); + Class targetClassClass = target.getClass(); + // 类地址 + container.put("class", targetClassClass.toGenericString()); + // 方法名称 + container.put("methodName", method.getName()); + // 包名称 + container.put("package", targetClassClass.getPackage()); + // 参数列表 + for (int i = 0; i < params.length; i++) { + container.put(String.valueOf(i), params[i]); + } + // 转为JSON字符串 + String jsonString = JSON.toJSONString(container); + // 做SHA256 Hash计算,得到一个SHA256摘要作为Key + return DigestUtils.sha256Hex(jsonString); + }; + } + + @Bean + @Override + public CacheErrorHandler errorHandler() { + // 异常处理,当Redis发生异常时,打印日志,但是程序正常走 + log.info("初始化 -> [{}]", "Redis CacheErrorHandler"); + return new CacheErrorHandler() { + @Override + public void handleCacheGetError(RuntimeException e, Cache cache, Object key) { + log.error("Redis occur handleCacheGetError:key -> [{}]", key, e); + } + + @Override + public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) { + log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e); + } + + @Override + public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { + log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e); + } + + @Override + public void handleCacheClearError(RuntimeException e, Cache cache) { + log.error("Redis occur handleCacheClearError:", e); + } + }; + } + +} + +/** + * Value 序列化 + * + * @param + */ +class FastJsonRedisSerializer implements RedisSerializer { + + private Class clazz; + + FastJsonRedisSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) { + if (t == null) { + return new byte[0]; + } + return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8); + } + + @Override + public T deserialize(byte[] bytes) { + if (bytes == null || bytes.length <= 0) { + return null; + } + String str = new String(bytes, StandardCharsets.UTF_8); + return JSON.parseObject(str, clazz); + } + +} + +/** + * 重写序列化器 + * + */ +class StringRedisSerializer implements RedisSerializer { + + private final Charset charset; + + StringRedisSerializer() { + this(StandardCharsets.UTF_8); + } + + private StringRedisSerializer(Charset charset) { + Assert.notNull(charset, "Charset must not be null!"); + this.charset = charset; + } + + @Override + public String deserialize(byte[] bytes) { + return (bytes == null ? null : new String(bytes, charset)); + } + + @Override + public byte[] serialize(Object object) { + String string = JSON.toJSONString(object); + if (StringUtils.isBlank(string)) { + return null; + } + string = string.replace("\"", ""); + return string.getBytes(charset); + } +} diff --git a/dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/utils/RedisUtils.java b/dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/utils/RedisUtils.java new file mode 100644 index 0000000..16e8d09 --- /dev/null +++ b/dubhe-server/common-biz/redis/src/main/java/org/dubhe/biz/redis/utils/RedisUtils.java @@ -0,0 +1,865 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.biz.redis.utils; + +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisConnectionUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @description redis工具类 + * @date 2020-03-13 + */ +@Component +@SuppressWarnings({"unchecked", "all"}) +public class RedisUtils { + + private RedisTemplate redisTemplate; + @Value("${jwt.online-key}") + private String onlineKey; + + public RedisUtils(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + // =============================common============================ + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) + */ + public boolean expire(String key, long time) { + try { + if (time > 0) { + redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils expire key {} time {} error:{}", key, time, e); + return false; + } + return true; + } + + /** + * 根据 key 获取过期时间 + * + * @param key 键 不能为null + * @return 时间(秒) 返回0代表为永久有效 + */ + public long getExpire(Object key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + /** + * 查找匹配key + * + * @param pattern key + * @return List 匹配的key集合 + */ + public List scan(String pattern) { + ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); + RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(); + while (cursor.hasNext()) { + result.add(new String(cursor.next())); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory, true); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils scan pattern {} error:{}", pattern, e); + } + return result; + } + + /** + * 分页查询 key + * + * @param patternKey key + * @param page 页码 + * @param size 每页数目 + * @return 匹配到的key集合 + */ + public List findKeysForPage(String patternKey, int page, int size) { + ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); + RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(size); + int tmpIndex = 0; + int fromIndex = page * size; + int toIndex = page * size + size; + while (cursor.hasNext()) { + if (tmpIndex >= fromIndex && tmpIndex < toIndex) { + result.add(new String(cursor.next())); + tmpIndex++; + continue; + } + // 获取到满足条件的数据后,就可以退出了 + if (tmpIndex >= toIndex) { + break; + } + tmpIndex++; + cursor.next(); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory, true); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils findKeysForPage patternKey {} page {} size {} error:{}", patternKey, page, size, e); + } + return result; + } + + /** + * 判断key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public boolean hasKey(String key) { + try { + return redisTemplate.hasKey(key); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils hasKey key {} error:{}", key, e); + return false; + } + } + + /** + * 删除缓存 + * + * @param key 可以传一个值 或多个 + */ + public void del(String... key) { + if (key != null && key.length > 0) { + if (key.length == 1) { + redisTemplate.delete(key[0]); + } else { + redisTemplate.delete(CollectionUtils.arrayToList(key)); + } + } + } + + /** + * + * @param script 脚本字符串 + * @param key 键 + * @param args 脚本其他参数 + * @return + */ + public Object executeRedisScript(String script, String key, Object... args) { + try { + RedisScript redisScript = new DefaultRedisScript<>(script, Long.class); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + return redisTemplate.execute(redisScript, Collections.singletonList(key), args); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "executeRedisScript script {} key {} args {} error:{}", script, key, args, e); + return MagicNumConstant.ZERO_LONG; + } + } + + /** + * + * @param script 脚本字符串 + * @param key 键 + * @param args 脚本其他参数 + * @return + */ + public Object executeRedisObjectScript(String script, String key, Object... args) { + try { + RedisScript redisScript = new DefaultRedisScript<>(script, Object.class); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + return redisTemplate.execute(redisScript, Collections.singletonList(key), args); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "executeRedisObjectScript script {} key {} args {} error:{}", script, key, args, e); + return null; + } + } + + // ============================String============================= + + /** + * 普通缓存获取 + * + * @param key 键 + * @return key对应的value值 + */ + public Object get(String key) { + + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + /** + * 批量获取 + * + * @param keys key集合 + * @return key集合对应的value集合 + */ + public List multiGet(List keys) { + Object obj = redisTemplate.opsForValue().multiGet(Collections.singleton(keys)); + return null; + } + + /** + * 普通缓存放入 + * + * @param key 键 + * @param value 值 + * @return true成功 false失败 + */ + public boolean set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils set key {} value {} error:{}", key, value, e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils set key {} value {} time {} error:{}", key, value, time, e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间 + * @param timeUnit 类型 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, timeUnit); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils set key {} value {} time {} timeUnit {} error:{}", key, value, time, timeUnit, e); + return false; + } + } + + //===============================Lock================================= + + /** + * 加锁 + * @param key 键 + * @param requestId 请求id用以释放锁 + * @param expireTime 超时时间(秒) + * @return + */ + public boolean getDistributedLock(String key, String requestId, long expireTime) { + String script = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end else return 0 end"; + Object result = executeRedisScript(script, key, requestId, expireTime); + return result != null && result.equals(MagicNumConstant.ONE_LONG); + } + + /** + * 释放锁 + * @param key 键 + * @param requestId 请求id用以释放锁 + * @return + */ + public boolean releaseDistributedLock(String key, String requestId) { + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + Object result = executeRedisScript(script, key, requestId); + return result != null && result.equals(MagicNumConstant.ONE_LONG); + } + + // ================================Map================================= + + /** + * HashGet + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return 值 + */ + public Object hget(String key, String item) { + return redisTemplate.opsForHash().get(key, item); + } + + /** + * 获取hashKey对应的所有键值 + * + * @param key 键 + * @return 对应的多个键值 + */ + public Map hmget(String key) { + return redisTemplate.opsForHash().entries(key); + + } + + /** + * HashSet + * + * @param key 键 + * @param map 对应多个键值 + * @return true 成功 false 失败 + */ + public boolean hmset(String key, Map map) { + try { + redisTemplate.opsForHash().putAll(key, map); + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils hmset key {} map {} error:{}", key, map, e); + return false; + } + } + + /** + * HashSet 并设置时间 + * + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) + * @return true成功 false失败 + */ + public boolean hmset(String key, Map map, long time) { + try { + redisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils hmset key {} map {} time {} error:{}", key, map, time, e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value) { + try { + redisTemplate.opsForHash().put(key, item, value); + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils hset key {} item {} value {} error:{}", key, item, value, e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value, long time) { + try { + redisTemplate.opsForHash().put(key, item, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils hset key {} item {} value {} time {} error:{}", key, item, value, time, e); + return false; + } + } + + /** + * 删除hash表中的值 + * + * @param key 键 不能为null + * @param item 项 可以使多个 不能为null + */ + public void hdel(String key, Object... item) { + redisTemplate.opsForHash().delete(key, item); + } + + /** + * 判断hash表中是否有该项的值 + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return true 存在 false不存在 + */ + public boolean hHasKey(String key, String item) { + return redisTemplate.opsForHash().hasKey(key, item); + } + + /** + * hash递增 如果不存在,就会创建一个 并把新增后的值返回 + * + * @param key 键 + * @param item 项 + * @param by 要增加几(大于0) + * @return + */ + public double hincr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, by); + } + + /** + * hash递减 + * + * @param key 键 + * @param item 项 + * @param by 要减少记(小于0) + * @return + */ + public double hdecr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, -by); + } + + // ============================set============================= + + /** + * 根据key获取Set中的所有值 + * + * @param key 键 + * @return + */ + public Set sGet(String key) { + try { + return redisTemplate.opsForSet().members(key); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils sGet key {} error:{}", key, e); + return null; + } + } + + /** + * 根据value从一个set中查询,是否存在 + * + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + */ + public boolean sHasKey(String key, Object value) { + try { + return redisTemplate.opsForSet().isMember(key, value); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils sHasKey key {} value {} error:{}", key, value, e); + return false; + } + } + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSet(String key, Object... values) { + try { + return redisTemplate.opsForSet().add(key, values); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils sSet key {} values {} error:{}", key, values, e); + return 0; + } + } + + /** + * 将set数据放入缓存 + * + * @param key 键 + * @param time 时间(秒) + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSetAndTime(String key, long time, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + if (time > 0) { + expire(key, time); + } + return count; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils sSetAndTime key {} time {} values {} error:{}", key, time, values, e); + return 0; + } + } + + /** + * 获取set缓存的长度 + * + * @param key 键 + * @return + */ + public long sGetSetSize(String key) { + try { + return redisTemplate.opsForSet().size(key); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils sGetSetSize key {} error:{}", key, e); + return 0; + } + } + + /** + * 移除值为value的 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + */ + public long setRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().remove(key, values); + return count; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils setRemove key {} values {} error:{}", key, values, e); + return 0; + } + } + + // ===============================sorted set================================= + + /** + * 将zSet数据放入缓存 + * + * @param key + * @param time + * @param values + * @return Boolean + */ + public Boolean zSet(String key, long time, Object value) { + try { + Boolean success = redisTemplate.opsForZSet().add(key, value, System.currentTimeMillis()); + if (success) { + expire(key, time); + } + return success; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils zSet key {} time {} value {} error:{}", key, time, value, e); + return false; + } + } + + /** + * 将zSet数据放入缓存 + * @param key 健 + * @param score 分数 + * @param value 值 + * @return + */ + public Boolean zAdd(String key,Long score,Object value){ + try{ + if (StringUtils.isEmpty(key) || score == null || value == null){ + return false; + } + return redisTemplate.opsForZSet().add(key, value, score); + }catch (Exception e){ + LogUtil.error(LogEnum.REDIS, "RedisUtils zAdd key {} score {} value {} error:{}", key, score, value, e); + return false; + } + } + + /** + * 返回有序集合所有成员,从大到小排序 + * + * @param key + * @return Set + */ + public Set zGet(String key) { + try { + return redisTemplate.opsForZSet().reverseRange(key, Long.MIN_VALUE, Long.MAX_VALUE); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils zGet key {} error:{}", key, e); + return null; + } + } + + /** + * 弹出有序集合 score 在 [min,max] 内由小到大从 offset 取 count 个 + * @param key 健 + * @param min score最小值 + * @param max score最大值 + * @param offset 起始下标 + * @param count 偏移量 + * @return + */ + public List zRangeByScorePop(String key,double min,double max,long offset,long count){ + try{ + String script = "local elementSet = redis.call('ZRANGEBYSCORE',KEYS[1],ARGV[1],ARGV[2],'LIMIT',ARGV[3],ARGV[4]) if elementSet ~= false and #elementSet ~= 0 then redis.call('ZREM' , KEYS[1] , elementSet[1]) end return elementSet"; + Object result = executeRedisObjectScript(script, key, min, max,offset,count); + return (List) result; + }catch (Exception e){ + LogUtil.error(LogEnum.REDIS, "RedisUtils zRangeByScorePop key {} min {} max {} offset {} count {} error:{}", key,min, max, offset, count, e); + return new ArrayList<>(); + } + } + + /** + * 弹出有序集合 score 在 [min,max] 内由小到大从 0 取 1 个 + * @param key 健 + * @param min score最小值 + * @param max score最大值 + * @return + */ + public List zRangeByScorePop(String key,double min, double max){ + return zRangeByScorePop( key,min, max,0,1); + } + + /** + * 弹出有序集合 score 在 [0,max] 内由小到大从 offset 取 count 个 + * @param key 健 + * @param max score最大值 + * @return + */ + public List zRangeByScorePop(String key,double max){ + return zRangeByScorePop( key,0, max,0,1); + } + + + // ===============================list================================= + + /** + * 获取list缓存的内容 + * + * @param key 键 + * @param start 开始 + * @param end 结束 0 到 -1代表所有值 + * @return + */ + public List lGet(String key, long start, long end) { + try { + return redisTemplate.opsForList().range(key, start, end); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lGetIndex key {} start {} end {} error:{}", key, start, end, e); + return null; + } + } + + /** + * 获取list缓存的长度 + * + * @param key 键 + * @return + */ + public long lGetListSize(String key) { + try { + return redisTemplate.opsForList().size(key); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lGetListSize key {} error:{}", key, e); + return 0; + } + } + + /** + * 通过索引 获取list中的值 + * + * @param key 键 + * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 + * @return + */ + public Object lGetIndex(String key, long index) { + try { + return redisTemplate.opsForList().index(key, index); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lGetIndex key {} index {} error:{}", key, index, e); + return null; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public boolean lSet(String key, Object value) { + try { + redisTemplate.opsForList().rightPush(key, value); + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lSet key {} value {} error:{}", key, value, e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return 是否存储成功 + */ + public boolean lSet(String key, Object value, long time) { + try { + redisTemplate.opsForList().rightPush(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lSet key {} value {} time {} error:{}", key, value, time, e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return 是否存储成功 + */ + public boolean lSet(String key, List value) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lSet key {} value {} error:{}", key, value, e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return 是否存储成功 + */ + public boolean lSet(String key, List value, long time) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lSet key {} value {} time {} error:{}", key, value, time, e); + return false; + } + } + + /** + * 根据索引修改list中的某条数据 + * + * @param key 键 + * @param index 索引 + * @param value 值 + * @return 更新数据标识 + */ + public boolean lUpdateIndex(String key, long index, Object value) { + try { + redisTemplate.opsForList().set(key, index, value); + return true; + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lUpdateIndex key {} index {} value {} error:{}", key, index, value, e); + return false; + } + } + + /** + * 移除N个值为value + * + * @param key 键 + * @param count 移除多少个 + * @param value 值 + * @return 移除的个数 + */ + public long lRemove(String key, long count, Object value) { + try { + return redisTemplate.opsForList().remove(key, count, value); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lRemove key {} count {} value {} error:{}", key, count, value, e); + return 0; + } + } + + /** + * 队列从左弹出数据 + * + * @param key + * @return key对应的value值 + */ + public Object lpop(String key) { + try { + return redisTemplate.opsForList().leftPop(key); + } catch (Exception e) { + LogUtil.error(LogEnum.REDIS, "RedisUtils lRemove key {} error:{}", key, e); + return null; + } + } + + /** + * 队列从右压入数据 + * + * @param key + * @param value + * @return long + */ + public long rpush(String key, Object value) { + try { + return redisTemplate.opsForList().rightPush(key, value); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "RedisUtils rpush key {} error:{}", key, e.getMessage()); + return 0L; + } + } +} diff --git a/dubhe-server/common-biz/state-machine/pom.xml b/dubhe-server/common-biz/state-machine/pom.xml new file mode 100644 index 0000000..5e3f2ff --- /dev/null +++ b/dubhe-server/common-biz/state-machine/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.dubhe.biz + common-biz + 0.0.1-SNAPSHOT + + state-machine + 0.0.1-SNAPSHOT + 状态机 + state machine for Dubhe Server + + + + + + org.apache.poi + poi + + + org.apache.poi + poi-ooxml + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + compile + + + + + + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/dto/StateChangeDTO.java b/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/dto/StateChangeDTO.java new file mode 100644 index 0000000..b6cbe93 --- /dev/null +++ b/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/dto/StateChangeDTO.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.statemachine.dto; + +import lombok.*; +import org.springframework.stereotype.Component; + +/** + * @description 执行状态机切换请求体 + * @date 2020-08-27 + */ +@Component +@Data +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class StateChangeDTO { + + /** + * 业务参数 eg: id + */ + private Object[] objectParam; + + /** + * 状态机类型 eg:dataStateMachine + */ + private String stateMachineType; + + /** + * 状态机执行事件 + */ + private String eventMethodName; + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/exception/StateMachineException.java b/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/exception/StateMachineException.java new file mode 100644 index 0000000..aa3c0fa --- /dev/null +++ b/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/exception/StateMachineException.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.statemachine.exception; + +import lombok.Getter; +import org.dubhe.biz.base.exception.BusinessException; + +/** + * @description 状态机异常类 + * @date 2020-08-27 + */ +@Getter +public class StateMachineException extends BusinessException { + + private static final long serialVersionUID = 1L; + + /** + * 自定义状态机异常(抛出异常堆栈信息) + * + * @param cause + */ + public StateMachineException(Throwable cause){ + super(cause); + } + + /** + * 自定义状态机异常(抛出异常信息) + * + * @param msg + */ + public StateMachineException(String msg){ + super(msg); + } + +} \ No newline at end of file diff --git a/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/utils/StateMachineProxyUtil.java b/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/utils/StateMachineProxyUtil.java new file mode 100644 index 0000000..600b92d --- /dev/null +++ b/dubhe-server/common-biz/state-machine/src/main/java/org/dubhe/biz/statemachine/utils/StateMachineProxyUtil.java @@ -0,0 +1,124 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.biz.statemachine.utils; + +import org.apache.commons.lang3.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.statemachine.dto.StateChangeDTO; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.statemachine.exception.StateMachineException; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; +import org.apache.poi.ss.formula.functions.T; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @description 代理执行状态机 + * @date 2020-08-27 + */ +@Component +public class StateMachineProxyUtil { + + /** + * 代理执行单个状态机的状态切换 + * + * @param stateChangeDTO 数据集状态切换信息 + * @param objectService 服务类 + */ + public static void proxyExecutionSingleState(StateChangeDTO stateChangeDTO, Object objectService) { + checkSingleParam(stateChangeDTO); + //获取全局状态机中的指定状态机 + Field field = ReflectionUtils.findField(objectService.getClass(), stateChangeDTO.getStateMachineType()); + if (field == null) { + throw new StateMachineException("The specified state machine was not found in the global state machine"); + } + //获取需要执行的状态机对象 + Object stateMachineObject = SpringContextHolder.getBean(field.getName()); + try { + //获取目标执行方法的参数类型 + List> paramTypesList = getMethodParamTypes(stateMachineObject, stateChangeDTO.getEventMethodName()); + //构造目标执行方法 + Method method = ReflectionUtils.findMethod(stateMachineObject.getClass(), stateChangeDTO.getEventMethodName(), paramTypesList.toArray(new Class[paramTypesList.size()])); + if (stateChangeDTO.getObjectParam().length != paramTypesList.size()) { + LogUtil.error(LogEnum.STATE_MACHINE, "Target execution method parameters {} Inconsistent with the number of incoming {} ", paramTypesList.size(), stateChangeDTO.getObjectParam().length); + } else { + ReflectionUtils.invokeMethod(method, stateMachineObject, stateChangeDTO.getObjectParam()); + } + } catch (ClassNotFoundException e) { + LogUtil.error(LogEnum.STATE_MACHINE, "The specified class was not found {} ", e); + throw new StateMachineException("The specified class was not found"); + } + } + + /** + * 代理执行多个状态机的状态切换 + * + * @param stateChangeDTOList 多个状态机切换信息 + * @param objectService 服务类 + */ + public static void proxyExecutionRelationState(List stateChangeDTOList,Object objectService) { + if (!CollectionUtils.isEmpty(stateChangeDTOList)) { + for (StateChangeDTO stateChangeDTO : stateChangeDTOList) { + proxyExecutionSingleState(stateChangeDTO,objectService); + } + } + } + + /** + * 校验参数是否正常 + * + * @param stateChangeDTO 数据集状态切换信息 + */ + public static void checkSingleParam(StateChangeDTO stateChangeDTO) { + if (StringUtils.isEmpty(stateChangeDTO.getStateMachineType())) { + throw new StateMachineException("No state machine class specified"); + } + if (StringUtils.isEmpty(stateChangeDTO.getEventMethodName())) { + throw new StateMachineException("The state machine is not specified and events need to be executed"); + } + } + + /** + * 根据方法名获取所有参数的类型 + * + * @param classInstance 类实例 + * @param methodName 方法名 + * @return List> 对象集合 + * @throws ClassNotFoundException + */ + public static List> getMethodParamTypes(Object classInstance, String methodName) throws ClassNotFoundException { + List> paramTypes = new ArrayList<>(); + Method[] methods = classInstance.getClass().getMethods(); + for (Method method : methods) { + if (method.getName().equals(methodName)) { + Class[] params = method.getParameterTypes(); + for (Class classParamType : params) { + paramTypes.addAll(Collections.singleton((Class) Class.forName(classParamType.getName()))); + } + break; + } + } + return paramTypes; + } + +} diff --git a/dubhe-server/common-cloud/auth-config/pom.xml b/dubhe-server/common-cloud/auth-config/pom.xml new file mode 100644 index 0000000..29a7aa7 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + org.dubhe.cloud + common-cloud + 0.0.1-SNAPSHOT + + auth-config + 0.0.1-SNAPSHOT + Cloud 统一权限配置 + Authorization config for Spring Cloud + + + + + org.dubhe.biz + data-response + ${org.dubhe.biz.data-response.version} + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + + javax.servlet + javax.servlet-api + ${javax.servlet-api.version} + + + + org.springframework.cloud + spring-cloud-starter-security + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + + + org.dubhe.cloud + remote-call + ${org.dubhe.cloud.remote-call.version} + + + + + + + + + + + diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/config/ResourceServerConfig.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/config/ResourceServerConfig.java new file mode 100644 index 0000000..c513e63 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/config/ResourceServerConfig.java @@ -0,0 +1,126 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.config; + +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.cloud.authconfig.exception.handler.CustomerAccessDeniedHandler; +import org.dubhe.cloud.authconfig.exception.handler.CustomerTokenExceptionEntryPoint; +import org.dubhe.cloud.authconfig.factory.AccessTokenConverterFactory; +import org.dubhe.cloud.authconfig.factory.TokenServicesFactory; +import org.dubhe.cloud.authconfig.factory.TokenStoreFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.token.RemoteTokenServices; +import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.web.client.RestTemplate; + +import javax.sql.DataSource; + +/** + * @description 资源服务器鉴权配置 + * @date 2020-11-05 + */ +@Configuration +@EnableResourceServer +@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true) +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private DataSource dataSource; + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private CustomerAccessDeniedHandler accessDeniedHandler; + + @Autowired + private CustomerTokenExceptionEntryPoint tokenExceptionEntryPoint; + + @Value("${security.permitAll.matchers:}") + private String[] permitAllMatchers; + + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + + //配置异常处理 + resources.authenticationEntryPoint(tokenExceptionEntryPoint) + .accessDeniedHandler(accessDeniedHandler); + + resources + .tokenStore(tokenStore()) + .stateless(true); + // 配置RemoteTokenServices,用于向AuthorizationServer验证令牌 + RemoteTokenServices tokenServices = TokenServicesFactory.getTokenServices(accessTokenConverter(),restTemplate); + resources.tokenServices(tokenServices) + .stateless(true); + + + } + + @Bean + public JdbcTokenStore tokenStore(){ + return TokenStoreFactory.getJdbcTokenStore(dataSource); + } + + /** + * JWT转换器 + * @return + */ + @Bean + public JwtAccessTokenConverter accessTokenConverter(){ + return AccessTokenConverterFactory.getAccessTokenConverter(userDetailsService); + } + + + @Override + public void configure(HttpSecurity http) throws Exception { + // 配置资源服务器的拦截规则 + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .and() + .authorizeRequests() + // swagger + .antMatchers(getPermitAllMatchers()).permitAll() + .anyRequest().authenticated() + ; + } + + /** + * 获取匿名访问路径 + * @return + */ + private String[] getPermitAllMatchers(){ + String[] c= new String[permitAllMatchers.length + AuthConst.DEFAULT_PERMIT_PATHS.length]; + System.arraycopy(permitAllMatchers, 0, c, 0, permitAllMatchers.length); + System.arraycopy(AuthConst.DEFAULT_PERMIT_PATHS, 0, c, permitAllMatchers.length, AuthConst.DEFAULT_PERMIT_PATHS.length); + return c; + } + +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/decorator/AuthenticationThreadLocalTaskDecorator.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/decorator/AuthenticationThreadLocalTaskDecorator.java new file mode 100644 index 0000000..214b19c --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/decorator/AuthenticationThreadLocalTaskDecorator.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.cloud.authconfig.decorator; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.task.TaskDecorator; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * @description 线程池设置ThreadLocal用户信息 + * @date 2021-06-10 + */ +@Slf4j +public class AuthenticationThreadLocalTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return () -> { + try { + SecurityContextHolder.getContext().setAuthentication(authentication); + runnable.run(); + } catch (Throwable e){ + log.error(e.getMessage(), e); + } finally { + try { + SecurityContextHolder.getContext().setAuthentication(null); + } catch (Exception e){ + log.error(e.getMessage(), e); + throw new IllegalStateException(e); + } + } + }; + } +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/dto/JwtUserDTO.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/dto/JwtUserDTO.java new file mode 100644 index 0000000..7f05b98 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/dto/JwtUserDTO.java @@ -0,0 +1,92 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.dto; + +import org.dubhe.biz.base.context.UserContext; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +/** + * @description 安全用户模型 + * @date 2020-10-29 + */ +public class JwtUserDTO implements UserDetails{ + + private static final long serialVersionUID = 1L; + + private U user; + + private Collection authorities; + + public JwtUserDTO(U user, Collection authorities) { + this.user = user; + this.authorities = authorities; + } + + /** + * 获取当前用户ID + * @return + */ + public Long getCurUserId(){ + return user.getId(); + } + + /** + * 获取当前用户信息(不建议全部暴露,建议根据业务需要暴露可暴露信息) + * @return + */ + public U getUser() { + return user; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerAccessDeniedHandler.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerAccessDeniedHandler.java new file mode 100644 index 0000000..2f9e151 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerAccessDeniedHandler.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.exception.handler; + + +import com.alibaba.fastjson.JSONObject; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * @description 权限不足处理类 + * @date 2020-12-21 + */ +@Component +public class CustomerAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException{ + if(!Objects.isNull(e)){ + LogUtil.error(LogEnum.SYS_ERR,"CustomerTokenExceptionEntryPoint accessDeniedException is : {}",e); + } + httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + httpServletResponse.getWriter().write(JSONObject.toJSONString(DataResponseFactory.failed(ResponseCode.UNAUTHORIZED,"无权访问"))); + } +} \ No newline at end of file diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerTokenExceptionEntryPoint.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerTokenExceptionEntryPoint.java new file mode 100644 index 0000000..d6995f0 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/exception/handler/CustomerTokenExceptionEntryPoint.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.exception.handler; + + +import com.alibaba.fastjson.JSONObject; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + + +/** + * @description token无效处理类 + * @date 2020-12-21 + */ +@Component +public class CustomerTokenExceptionEntryPoint extends OAuth2AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException { + if(!Objects.isNull(e)){ + LogUtil.error(LogEnum.SYS_ERR,"CustomerTokenExceptionEntryPoint authenticationException is : {}",e); + } + httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + httpServletResponse.getWriter().write(JSONObject.toJSONString(DataResponseFactory.failed(ResponseCode.TOKEN_ERROR,"token无效或已过期"))); + } +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/AccessTokenConverterFactory.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/AccessTokenConverterFactory.java new file mode 100644 index 0000000..b3ddb05 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/AccessTokenConverterFactory.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.factory; + +import org.dubhe.biz.base.constant.AuthConst; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; + +/** + * @description AccessTokenConverter 工厂类 + * @date 2020-11-24 + */ +public class AccessTokenConverterFactory { + + private AccessTokenConverterFactory(){} + + + /** + * 获取JwtAccessTokenConverter + * @param userDetailsService 自定义实现的用户信息获取Service + * @return jwtAccessTokenConverter + */ + public static JwtAccessTokenConverter getAccessTokenConverter(UserDetailsService userDetailsService){ + DefaultUserAuthenticationConverter userAuthenticationConverter = new DefaultUserAuthenticationConverter(); + userAuthenticationConverter.setUserDetailsService(userDetailsService); + + DefaultAccessTokenConverter converter = new DefaultAccessTokenConverter(); + converter.setUserTokenConverter(userAuthenticationConverter); + + JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); + jwtAccessTokenConverter.setSigningKey(AuthConst.CLIENT_SECRET); + jwtAccessTokenConverter.setAccessTokenConverter(converter); + return jwtAccessTokenConverter; + } +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/PasswordEncoderFactory.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/PasswordEncoderFactory.java new file mode 100644 index 0000000..e67ca4e --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/PasswordEncoderFactory.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.cloud.authconfig.factory; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @description 加密工具工厂类 + * @date 2020-11-06 + */ +public class PasswordEncoderFactory { + + private PasswordEncoderFactory(){ + + } + + + public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); + + /** + * 获取加密器 + * @return + */ + public static PasswordEncoder getPasswordEncoder(){ + return PASSWORD_ENCODER; + } + +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenServicesFactory.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenServicesFactory.java new file mode 100644 index 0000000..40aaa54 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenServicesFactory.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.factory; + +import org.dubhe.biz.base.constant.AuthConst; +import org.springframework.security.oauth2.provider.token.RemoteTokenServices; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.web.client.RestTemplate; + +/** + * @description TokenServices 工厂类 + * @date 2020-11-25 + */ +public class TokenServicesFactory { + + private TokenServicesFactory(){ + + } + + /** + * 获取 RemoteTokenServices + * @param accessTokenConverter token转换器 + * @param restTemplate rest请求模板 + * @return RemoteTokenServices + */ + public static RemoteTokenServices getTokenServices(JwtAccessTokenConverter accessTokenConverter, RestTemplate restTemplate){ + RemoteTokenServices tokenServices = new RemoteTokenServices(); + if (accessTokenConverter != null){ + tokenServices.setAccessTokenConverter(accessTokenConverter); + } + // 配置异常处理器 +// restTemplate.setErrorHandler(new OAuth2ResponseErrorHandler()); + tokenServices.setRestTemplate(restTemplate); + tokenServices.setCheckTokenEndpointUrl(AuthConst.CHECK_TOKEN_ENDPOINT_URL); + tokenServices.setClientId(AuthConst.CLIENT_ID); + tokenServices.setClientSecret(AuthConst.CLIENT_SECRET); + return tokenServices; + } + +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenStoreFactory.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenStoreFactory.java new file mode 100644 index 0000000..99a4714 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/factory/TokenStoreFactory.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.factory; + +import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; + +import javax.sql.DataSource; + +/** + * @description TokenStore工厂类 + * @date 2020-11-24 + */ +public class TokenStoreFactory { + + private TokenStoreFactory(){ + + } + + /** + * 获取token存储 + * @param dataSource 数据库数据源 + * @return + */ + public static JdbcTokenStore getJdbcTokenStore(DataSource dataSource){ + return new JdbcTokenStore(dataSource); + } +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/filter/OAuth2ResponseErrorFilter.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/filter/OAuth2ResponseErrorFilter.java new file mode 100644 index 0000000..eb57fa5 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/filter/OAuth2ResponseErrorFilter.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.filter; + +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.dubhe.biz.base.exception.OAuthResponseError; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.ResourceAccessException; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @description OAuth2 授权异常 过滤器 + * @date 2020-11-18 + */ +@Slf4j +public class OAuth2ResponseErrorFilter implements Filter { + + /** + * 过滤并处理OAuth2异常抛出 + * + * @param servletRequest + * @param servletResponse + * @param filterChain + * @throws IOException + * @throws ServletException + */ + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletResponse response = ((HttpServletResponse) servletResponse); + try { + filterChain.doFilter(servletRequest,servletResponse); + }catch (OAuthResponseError e){ + log.error("鉴权失败!",e); + response.setStatus(e.getStatusCode().value()); + response.getOutputStream().write(JSON.toJSON(e.getResponseBody()).toString().getBytes()); + }catch (IllegalStateException | ResourceAccessException e){ + log.error("授权服务未发现!",e); + response.setStatus(HttpStatus.NOT_FOUND.value()); + response + .getOutputStream() + .write(JSON.toJSON(DataResponseFactory.failed(e.getMessage())).toString().getBytes()); + }catch (Exception e){ + log.error("授权异常!",e); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response + .getOutputStream() + .write(JSON.toJSON(DataResponseFactory.failed("OAuth2 response error!")).toString().getBytes()); + } + } + + +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClient.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClient.java new file mode 100644 index 0000000..8b826df --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClient.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.service; + +import org.dubhe.biz.base.constant.ApplicationNameConst; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.UserDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * @description admin远程服务调用类 + * @date 2020-12-10 + */ +@FeignClient(value = ApplicationNameConst.SERVER_ADMIN,fallback = AdminClientFallback.class) +public interface AdminClient { + + /** + * 根据用户名称获取用户信息 + * + * @param username 用户名称 + * @return 用户信息 + */ + @GetMapping(value = "/users/findUserByUsername") + DataResponseBody findUserByUsername(@RequestParam(value = "username") String username); + + @GetMapping(value = "/users/findById") + DataResponseBody getUsers(@RequestParam(value = "userId") Long userId); + + @GetMapping(value = "/users/findByIds") + DataResponseBody> getUserList(@RequestParam(value = "ids") List ids); + +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClientFallback.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClientFallback.java new file mode 100644 index 0000000..8bb550e --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminClientFallback.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.service; + +import org.dubhe.biz.base.dto.UserDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @description admin远程服务调用类熔断 + * @date 2020-12-10 + */ +@Component +public class AdminClientFallback implements AdminClient { + @Override + public DataResponseBody findUserByUsername(String username) { + return DataResponseFactory.failed("call admin server findUserByUsername error"); + } + + @Override + public DataResponseBody getUsers(Long userId) { + return DataResponseFactory.failed("call user controller to get user error"); + } + + @Override + public DataResponseBody> getUserList(List ids) { + return DataResponseFactory.failed("call user controller to get users error"); + } + +} \ No newline at end of file diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminUserService.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminUserService.java new file mode 100644 index 0000000..3827fe9 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/AdminUserService.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.service; + + +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.vo.DataResponseBody; + +/** + * @description Demo服务接口 + * @date 2020-11-26 + */ +public interface AdminUserService { + + /** + * 根据用户名查询用户信息 + * + * @param username 用户名称 + * @return 用户信息 + */ + DataResponseBody findUserByUsername(String username); + + + +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/GrantedAuthorityImpl.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/GrantedAuthorityImpl.java new file mode 100644 index 0000000..f538d2e --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/GrantedAuthorityImpl.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.service.impl; + +import org.springframework.security.core.GrantedAuthority; + +/** + * @description 权限封装 + * @date 2020-10-29 + */ +public class GrantedAuthorityImpl implements GrantedAuthority { + + private static final long serialVersionUID = 1L; + + private String authority; + + public GrantedAuthorityImpl(String authority) { + this.authority = authority; + } + + public void setAuthority(String authority) { + this.authority = authority; + } + + @Override + public String getAuthority() { + return this.authority; + } +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/OAuth2UserContextServiceImpl.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/OAuth2UserContextServiceImpl.java new file mode 100644 index 0000000..390f319 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/OAuth2UserContextServiceImpl.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.service.impl; + +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.cloud.authconfig.dto.JwtUserDTO; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.springframework.stereotype.Service; + +/** + * @description OAuth2 当前信息获取实现类 + * @date 2020-12-07 + */ +@Service(value = "oAuth2UserContextServiceImpl") +public class OAuth2UserContextServiceImpl implements UserContextService { + + @Override + public UserContext getCurUser() { + JwtUserDTO jwtUserDTO = JwtUtils.getCurUser(); + return jwtUserDTO == null?null:jwtUserDTO.getUser(); + } + + @Override + public Long getCurUserId() { + UserContext userContext = getCurUser(); + return userContext == null?null:userContext.getId(); + } +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/UserDetailsServiceImpl.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/UserDetailsServiceImpl.java new file mode 100644 index 0000000..0b034cd --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/service/impl/UserDetailsServiceImpl.java @@ -0,0 +1,99 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.service.impl; + +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.SysPermissionDTO; +import org.dubhe.biz.base.dto.SysRoleDTO; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.cloud.authconfig.dto.JwtUserDTO; +import org.dubhe.cloud.authconfig.service.AdminUserService; +import org.dubhe.cloud.authconfig.service.AdminClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @description 自定义用户实现类(未实现对应数据库持久化设置) + * @date 2020-10-29 + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Value("${spring.application.name}") + private String curService; + + @Autowired(required = false) + private AdminUserService adminUserService; + + @Resource + private AdminClient adminClient; + + /** + * 捞取用户信息 + * @param username + * @return + * @throws UsernameNotFoundException + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + //根据adminUserService 子类是否加载 采取不同的方式获取用户信息 + DataResponseBody responseBody = Objects.isNull(adminUserService) + ? adminClient.findUserByUsername(username) : adminUserService.findUserByUsername(username); + if (responseBody == null || Objects.isNull(responseBody.getData())){ + throw new BusinessException(responseBody.getMsg()); + } + UserContext data = responseBody.getData(); + // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('user')") 标注的接口对比,决定是否可以调用接口 + Set permissions = findPermissions(data.getRoles()); + List grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList()); + return new JwtUserDTO(data, grantedAuthorities); + } + + /** + * 固定权限 + * @return + */ + private Set findPermissions(List roles) { + Set permissions = new HashSet<>(); + roles.forEach(a->{ + permissions.add("ROLE_"+a.getName()); + List permissionsNames = a.getPermissions(); + if(!CollectionUtils.isEmpty(permissionsNames)){ + permissionsNames.forEach(b->{ + permissions.add("ROLE_"+b.getPermission()); + }); + + } + }); + return permissions; + } + +} diff --git a/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/utils/JwtUtils.java b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/utils/JwtUtils.java new file mode 100644 index 0000000..0768f42 --- /dev/null +++ b/dubhe-server/common-cloud/auth-config/src/main/java/org/dubhe/cloud/authconfig/utils/JwtUtils.java @@ -0,0 +1,63 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.authconfig.utils; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.cloud.authconfig.dto.JwtUserDTO; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * @description JWT + * @date 2020-11-25 + */ +public class JwtUtils { + + private JwtUtils(){ + + } + + /** + * 获取当前用户信息 + * @return 当前用户信息 + */ + public static JwtUserDTO getCurUser(){ + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(authentication == null){ + return null; + } + if (authentication.getPrincipal() instanceof JwtUserDTO){ + return (JwtUserDTO) authentication.getPrincipal(); + } + }catch (Exception e){ + LogUtil.error(LogEnum.SYS_ERR,"Jwt getCurUser error!{}",e); + } + return null; + } + + /** + * 获取当前用户ID + * 若用户不存在,则返回null(可根据业务统一修改) + * @return 当前用户ID + */ + public static Long getCurUserId(){ + JwtUserDTO jwtUserDTO = getCurUser(); + return jwtUserDTO == null?null:jwtUserDTO.getCurUserId(); + } +} diff --git a/dubhe-server/common-cloud/configuration/pom.xml b/dubhe-server/common-cloud/configuration/pom.xml new file mode 100644 index 0000000..939aa96 --- /dev/null +++ b/dubhe-server/common-cloud/configuration/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.dubhe.cloud + common-cloud + 0.0.1-SNAPSHOT + + configuration + 0.0.1-SNAPSHOT + Cloud 配置中心 + Configuration for Spring Cloud + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + ${nacos.version} + + + + + + + + + + + diff --git a/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-data.yml b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-data.yml new file mode 100644 index 0000000..23e7d7e --- /dev/null +++ b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-data.yml @@ -0,0 +1,7 @@ +spring: + cloud: + nacos: + config: + namespace: dubhe-server-cloud-test-data + discovery: + namespace: dubhe-server-cloud-test-data diff --git a/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-dev.yml b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-dev.yml new file mode 100644 index 0000000..0b3a9d3 --- /dev/null +++ b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-dev.yml @@ -0,0 +1,7 @@ +spring: + cloud: + nacos: + config: + namespace: dubhe-server-cloud-dev + discovery: + namespace: dubhe-server-cloud-dev diff --git a/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-test.yml b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-test.yml new file mode 100644 index 0000000..405d24e --- /dev/null +++ b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-cloud-test.yml @@ -0,0 +1,7 @@ +spring: + cloud: + nacos: + config: + namespace: dubhe-server-cloud-test + discovery: + namespace: dubhe-server-cloud-test diff --git a/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-dev.yml b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-dev.yml new file mode 100644 index 0000000..429a30f --- /dev/null +++ b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-dev.yml @@ -0,0 +1,7 @@ +spring: + cloud: + nacos: + config: + namespace: dubhe-server-cloud-prod + discovery: + namespace: dubhe-server-cloud-prod diff --git a/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-dev.yml b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-dev.yml new file mode 100644 index 0000000..f3895a8 --- /dev/null +++ b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-dev.yml @@ -0,0 +1,7 @@ +spring: + cloud: + nacos: + config: + namespace: dubhe-server-istio-dev + discovery: + namespace: dubhe-server-istio-dev diff --git a/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-test.yml b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-test.yml new file mode 100644 index 0000000..51c1cd8 --- /dev/null +++ b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-istio-test.yml @@ -0,0 +1,7 @@ +spring: + cloud: + nacos: + config: + namespace: dubhe-server-istio-test + discovery: + namespace: dubhe-server-istio-test diff --git a/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-prod.yml b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-prod.yml new file mode 100644 index 0000000..429a30f --- /dev/null +++ b/dubhe-server/common-cloud/configuration/src/main/resources/bootstrap-prod.yml @@ -0,0 +1,7 @@ +spring: + cloud: + nacos: + config: + namespace: dubhe-server-cloud-prod + discovery: + namespace: dubhe-server-cloud-prod diff --git a/dubhe-server/common-cloud/pom.xml b/dubhe-server/common-cloud/pom.xml new file mode 100644 index 0000000..7d9d1b4 --- /dev/null +++ b/dubhe-server/common-cloud/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + pom + + org.dubhe + server + 0.0.1-SNAPSHOT + + org.dubhe.cloud + common-cloud + 0.0.1-SNAPSHOT + 通用SpringCloud模块(Cloud) + Dubhe Common for Spring Cloud + + + auth-config + swagger + remote-call + registration + configuration + unit-test + + + + + + + + + + + + + + diff --git a/dubhe-server/common-cloud/registration/pom.xml b/dubhe-server/common-cloud/registration/pom.xml new file mode 100644 index 0000000..08cf43f --- /dev/null +++ b/dubhe-server/common-cloud/registration/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.dubhe.cloud + common-cloud + 0.0.1-SNAPSHOT + + registration + 0.0.1-SNAPSHOT + Cloud 注册中心 + Registration for Spring Cloud + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + ${nacos.version} + + + + + + + + + + diff --git a/dubhe-server/common-cloud/registration/src/main/java/org/dubhe/cloud/registration/RegistrationConfig.java b/dubhe-server/common-cloud/registration/src/main/java/org/dubhe/cloud/registration/RegistrationConfig.java new file mode 100644 index 0000000..ba13a2d --- /dev/null +++ b/dubhe-server/common-cloud/registration/src/main/java/org/dubhe/cloud/registration/RegistrationConfig.java @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.registration; + +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.Configuration; + +/** + * @description 注册中心默认配置 + * @date 2020-12-11 + */ +@Configuration +@EnableDiscoveryClient +public class RegistrationConfig { +} diff --git a/dubhe-server/common-cloud/remote-call/pom.xml b/dubhe-server/common-cloud/remote-call/pom.xml new file mode 100644 index 0000000..e382c34 --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.dubhe.cloud + common-cloud + 0.0.1-SNAPSHOT + + remote-call + 0.0.1-SNAPSHOT + Cloud 远程调用 + Remote call for Spring Cloud + + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + + + + org.springframework.cloud + spring-cloud-openfeign-core + + + org.springframework.cloud + spring-cloud-starter-feign + ${feign.version} + + + io.github.openfeign + feign-httpclient + + + + javax.servlet + javax.servlet-api + ${javax.servlet-api.version} + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + io.github.openfeign + feign-httpclient + + + + + + + + + + diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/FeignErrorDecoder.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/FeignErrorDecoder.java new file mode 100644 index 0000000..9f15754 --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/FeignErrorDecoder.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.cloud.remotecall.config; + + + + +import com.alibaba.fastjson.JSONObject; +import feign.Response; +import feign.Util; +import feign.codec.ErrorDecoder; +import org.dubhe.biz.base.exception.FeignException; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.context.annotation.Configuration; + + +/** + * @description feign 异常处理类 + * @date 2020-12-21 + */ + +@Configuration +public class FeignErrorDecoder implements ErrorDecoder { + + @Override + public Exception decode(String s, Response response) { + FeignException baseException = null; + try { + String errorContent = Util.toString(response.body().asReader()); + DataResponseBody result = JSONObject.parseObject(errorContent, DataResponseBody.class); + baseException = new FeignException(result.getCode(), result.getMsg()); + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR,"FeignClient error :{}",e); + } + return baseException; + } +} diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/HttpClientConfig.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/HttpClientConfig.java new file mode 100644 index 0000000..58e60d8 --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/HttpClientConfig.java @@ -0,0 +1,125 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.remotecall.config; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static org.dubhe.biz.base.constant.MagicNumConstant.ONE_THOUSAND; + +/** + * @description HttpClient配置类 + * @date 2020-12-28 + */ +@Configuration +@AutoConfigureBefore(FeignAutoConfiguration.class) +public class HttpClientConfig { + + /** + * 超时时间(秒) + */ + private final static int TIMEOUT_SECOND = MagicNumConstant.FIVE; + + private final ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, + new BasicThreadFactory.Builder().namingPattern("RemoteCall HTTP连接回收管理器-%d").daemon(true).build()); + + /** + * 构建HttpClient + * @return HttpClient + */ + @Bean + public CloseableHttpClient httpClient(){ + int processCount = Runtime.getRuntime().availableProcessors() * 2; + return httpClientBuilder(processCount).build(); + } + + /** + * 获取Http客户端生成器 + * @param processCount 当前硬件线程数*2 + * @return HttpClientBuilder + */ + private HttpClientBuilder httpClientBuilder(int processCount){ + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + // Keep Alive 默认策略 + httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy()); + // 连接管理器 + httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager(processCount)); + // Request请求配置 + httpClientBuilder.setDefaultRequestConfig(requestConfig()); + // 重试策略 + httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true)); + return httpClientBuilder; + } + + /** + * 获取Request请求配置 + * @return RequestConfig + */ + private RequestConfig requestConfig(){ + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + // Socket超时时间(毫秒) + requestConfigBuilder.setSocketTimeout(TIMEOUT_SECOND * ONE_THOUSAND); + // 连接超时时间(毫秒) + requestConfigBuilder.setConnectTimeout(TIMEOUT_SECOND * ONE_THOUSAND); + // 从连接管理器中请求连接的超时时间(毫秒) + requestConfigBuilder.setConnectionRequestTimeout(1 * ONE_THOUSAND); + return requestConfigBuilder.build(); + } + + /** + * 获取http连接管理器 + * @param processCount 当前硬件线程数*2 + * @return PoolingHttpClientConnectionManager + */ + private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager(int processCount){ + PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(1, TimeUnit.MINUTES); + // 总连接数 + manager.setMaxTotal(processCount); + // 同一个路由并发数控制 + manager.setDefaultMaxPerRoute(processCount/2); + // 连接不活跃1秒后验证有效性 + manager.setValidateAfterInactivity(1 * ONE_THOUSAND); + executorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + LogUtil.debug(LogEnum.REMOTE_CALL,"PoolingHttpClientConnectionManager time to close connection.. "); + // 关闭过期连接 + manager.closeExpiredConnections(); + // 关闭空闲5秒连接 + manager.closeIdleConnections(TIMEOUT_SECOND,TimeUnit.SECONDS); + } + },10,5, TimeUnit.SECONDS); + return manager; + } + +} diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RemoteCallConfig.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RemoteCallConfig.java new file mode 100644 index 0000000..d0deac0 --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RemoteCallConfig.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.remotecall.config; + +import org.dubhe.biz.base.constant.AuthConst; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +import java.util.LinkedList; +import java.util.List; + +/** + * @description 远程调用默认配置 + * @date 2020-12-11 + */ +@Configuration +@EnableFeignClients(basePackages = "org.dubhe") +public class RemoteCallConfig { + + /** + * 待处理token列表 + */ + public static final List TOKEN_LIST = new LinkedList<>(); + + static { + TOKEN_LIST.add(AuthConst.AUTHORIZATION); + TOKEN_LIST.add(AuthConst.K8S_CALLBACK_TOKEN); + TOKEN_LIST.add(AuthConst.COMMON_TOKEN); + } +} diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateConfig.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateConfig.java new file mode 100644 index 0000000..5b5c45f --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateConfig.java @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.remotecall.config; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.dubhe.cloud.remotecall.interceptor.RestTemplateTokenInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; + +/** + * @description RestTemplate配置类 + * @date 2020-11-26 + */ +@Configuration +@ConditionalOnMissingBean(RestTemplate.class) +public class RestTemplateConfig { + + @Autowired + private CloseableHttpClient httpClient; + + /** + * 负载均衡 + * @return RestTemplate + */ + @LoadBalanced + @Bean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate( + new HttpComponentsClientHttpRequestFactory(httpClient) + ); + //拦截器统一添加token + restTemplate.setInterceptors( + Collections.singletonList( + new RestTemplateTokenInterceptor() + ) + ); + return restTemplate; + } + +} diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateHolder.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateHolder.java new file mode 100644 index 0000000..0fe7b4e --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/config/RestTemplateHolder.java @@ -0,0 +1,95 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.cloud.remotecall.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + + +/** + * @description RestTemplate持有器(调用非nacos中注册的服务) + * @date 2021-01-07 + */ +@Component +public class RestTemplateHolder { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private RestTemplateBuilder restTemplateBuilder; + + private RestTemplate restTemplate; + + @PostConstruct + private void init() { + RestTemplate template = restTemplateBuilder.build(); + + MediaType[] mediaTypes = new MediaType[]{ + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_OCTET_STREAM, + MediaType.TEXT_HTML, + MediaType.TEXT_PLAIN, + MediaType.TEXT_XML, + MediaType.APPLICATION_STREAM_JSON, + MediaType.APPLICATION_ATOM_XML, + MediaType.APPLICATION_FORM_URLENCODED, + MediaType.APPLICATION_JSON_UTF8, + MediaType.APPLICATION_PDF, + }; + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + converter.setSupportedMediaTypes(Arrays.asList(mediaTypes)); + + try { + Field field = template.getClass().getDeclaredField("messageConverters"); + field.setAccessible(true); + List> orgConverterList = (List>) field.get(template); + + for (int i = 0; i < orgConverterList.size(); i++) { + if (MappingJackson2HttpMessageConverter.class.equals(orgConverterList.get(i).getClass())) { + orgConverterList.set(i, converter); + } + } + + } catch (Exception e) { + LogUtil.error(LogEnum.SYS_ERR, "restTemplate bean instance error, exception is :【{}】", e); + } + this.restTemplate = template; + } + + public RestTemplate getRestTemplate(){ + return this.restTemplate; + } + + +} diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/FeignInterceptor.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/FeignInterceptor.java new file mode 100644 index 0000000..07c7e74 --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/FeignInterceptor.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.remotecall.interceptor; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.dubhe.cloud.remotecall.config.RemoteCallConfig; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + * @description Feign请求拦截器 + * @date 2020-11-19 + */ +@Configuration +public class FeignInterceptor implements RequestInterceptor { + + /** + * 拦截feign请求,传递token + * @param requestTemplate + */ + @Override + public void apply(RequestTemplate requestTemplate) { + if (RequestContextHolder.getRequestAttributes() == null){ + return; + } + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = attributes.getRequest(); + RemoteCallConfig.TOKEN_LIST.forEach(token -> { + requestTemplate.header(token, request.getHeader(token)); + }); + } +} diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/RestTemplateTokenInterceptor.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/RestTemplateTokenInterceptor.java new file mode 100644 index 0000000..dda819c --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/interceptor/RestTemplateTokenInterceptor.java @@ -0,0 +1,97 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.remotecall.interceptor; + +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.cloud.remotecall.config.RemoteCallConfig; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.List; + +/** + * @description RestTemplate token拦截处理器 + * @date 2020-11-26 + */ +public class RestTemplateTokenInterceptor implements ClientHttpRequestInterceptor { + + /** + * 对 RestTemplate 的token进行拦截传递 + * @param httpRequest the request, containing method, URI, and headers + * @param body the body of the request + * @param execution the request execution + * @return the response + * @throws IOException in case of I/O errors + */ + @Override + public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution execution) throws IOException { + RemoteCallConfig.TOKEN_LIST.forEach(token -> { + if (AuthConst.AUTHORIZATION.equals(token)){ + do4Auth2Token(httpRequest); + }else { + do4DefinedToken(httpRequest,token); + } + }); + return execution.execute(httpRequest, body); + } + + /** + * 处理自定义token传递 + * @param httpRequest the request, containing method, URI, and headers + * @param token token名称 + */ + private void do4DefinedToken(HttpRequest httpRequest,String token) { + List authorizations = httpRequest.getHeaders().get(token); + if (CollectionUtils.isEmpty(authorizations)){ + return; + } + httpRequest.getHeaders().add(token, authorizations.get(0)); + } + + /** + * 处理Auth2授权token传递 + * @param httpRequest the request, containing method, URI, and headers + */ + private void do4Auth2Token(HttpRequest httpRequest) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes == null){ + return; + } + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + String authToken = request.getHeader(AuthConst.AUTHORIZATION); + String accessToken = request.getParameter(AuthConst.ACCESS_TOKEN); + List authorizations = httpRequest.getHeaders().get(AuthConst.AUTHORIZATION); + String authorization = null; + if (accessToken != null){ + authorization = AuthConst.ACCESS_TOKEN_PREFIX + accessToken; + }else if (authToken != null){ + authorization = authToken; + }else if (authorizations != null){ + authorization = AuthConst.ACCESS_TOKEN_PREFIX + authorizations.get(0); + } + httpRequest.getHeaders().add(AuthConst.AUTHORIZATION, authorization); + } + +} diff --git a/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/strategy/RequestAttributeHystrixConcurrencyStrategy.java b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/strategy/RequestAttributeHystrixConcurrencyStrategy.java new file mode 100644 index 0000000..da23ba8 --- /dev/null +++ b/dubhe-server/common-cloud/remote-call/src/main/java/org/dubhe/cloud/remotecall/strategy/RequestAttributeHystrixConcurrencyStrategy.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dubhe.cloud.remotecall.strategy; + +import com.netflix.hystrix.HystrixThreadPoolKey; +import com.netflix.hystrix.HystrixThreadPoolProperties; +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; +import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; +import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; +import com.netflix.hystrix.strategy.properties.HystrixProperty; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @description 重写Hystrix参数传递并发处理战略,保证授权token传递 + * 参考 org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixConcurrencyStrategy + * @date 2020-11-26 + */ +@Slf4j +@Component +public class RequestAttributeHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy { + + private HystrixConcurrencyStrategy delegate; + + public RequestAttributeHystrixConcurrencyStrategy() { + this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy(); + if (this.delegate instanceof RequestAttributeHystrixConcurrencyStrategy) { + return; + } + HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins + .getInstance().getCommandExecutionHook(); + HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance() + .getEventNotifier(); + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance() + .getMetricsPublisher(); + HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance() + .getPropertiesStrategy(); + this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, + propertiesStrategy); + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerConcurrencyStrategy(this); + HystrixPlugins.getInstance() + .registerCommandExecutionHook(commandExecutionHook); + HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); + HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); + HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); + } + + private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier, + HystrixMetricsPublisher metricsPublisher, + HystrixPropertiesStrategy propertiesStrategy) { + if (log.isDebugEnabled()) { + log.debug("Current Hystrix plugins configuration is [" + + "concurrencyStrategy [" + this.delegate + "]," + "eventNotifier [" + + eventNotifier + "]," + "metricPublisher [" + metricsPublisher + "]," + + "propertiesStrategy [" + propertiesStrategy + "]," + "]"); + log.debug("Registering Sleuth Hystrix Concurrency Strategy."); + } + } + + @Override + public Callable wrapCallable(Callable callable) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + return new WrappedCallable<>(callable, requestAttributes); + } + + static class WrappedCallable implements Callable { + + private final Callable target; + // 基于成员变量,手动传递RequestContextHolder.getRequestAttributes(); + private final RequestAttributes requestAttributes; + + public WrappedCallable(Callable target, RequestAttributes requestAttributes) { + this.target = target; + this.requestAttributes = requestAttributes; + } + + @Override + public T call() throws Exception { + try { + RequestContextHolder.setRequestAttributes(requestAttributes); + return target.call(); + }finally { + RequestContextHolder.resetRequestAttributes(); + } + } + } + + @Override + public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, + HystrixProperty corePoolSize, + HystrixProperty maximumPoolSize, + HystrixProperty keepAliveTime, TimeUnit unit, + BlockingQueue workQueue) { + return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, + keepAliveTime, unit, workQueue); + } + + @Override + public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, + HystrixThreadPoolProperties threadPoolProperties) { + return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties); + } + + @Override + public BlockingQueue getBlockingQueue(int maxQueueSize) { + return this.delegate.getBlockingQueue(maxQueueSize); + } + + @Override + public HystrixRequestVariable getRequestVariable( + HystrixRequestVariableLifecycle rv) { + return this.delegate.getRequestVariable(rv); + } +} diff --git a/dubhe-server/common-cloud/swagger/pom.xml b/dubhe-server/common-cloud/swagger/pom.xml new file mode 100644 index 0000000..667949a --- /dev/null +++ b/dubhe-server/common-cloud/swagger/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.dubhe.cloud + common-cloud + 0.0.1-SNAPSHOT + + swagger + 0.0.1-SNAPSHOT + Cloud swagger + Swagger for Spring Cloud + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + + io.springfox + springfox-swagger2 + ${swagger.version} + + + com.github.xiaoymin + swagger-bootstrap-ui + ${swagger-bootstrap-ui.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework + spring-webmvc + + + + + + + + + diff --git a/dubhe-server/common-cloud/swagger/src/main/java/org/dubhe/cloud/swagger/config/SwaggerConfig.java b/dubhe-server/common-cloud/swagger/src/main/java/org/dubhe/cloud/swagger/config/SwaggerConfig.java new file mode 100644 index 0000000..fda245b --- /dev/null +++ b/dubhe-server/common-cloud/swagger/src/main/java/org/dubhe/cloud/swagger/config/SwaggerConfig.java @@ -0,0 +1,134 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.cloud.swagger.config; + +import com.fasterxml.classmate.TypeResolver; +import com.google.common.base.Predicates; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.biz.base.constant.AuthConst; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.data.domain.Pageable; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.AlternateTypeRule; +import springfox.documentation.schema.AlternateTypeRuleConvention; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; +import static springfox.documentation.schema.AlternateTypeRules.newRule; + +/** + * @description api页面 /swagger-ui.html + * @date 2020-03-25 + */ +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + /** + * 默认开启,如需要关闭,再对应模块自行关闭 + */ + @Value("${swagger.enabled:true}") + private Boolean enabled; + + @Value("${spring.application.name}") + private String applicationName; + + + @Bean + @SuppressWarnings("all") + public Docket createRestApi() { + List pars = new ArrayList<>(); + pars.add(new ParameterBuilder() + .name(AuthConst.AUTHORIZATION) + .description("令牌") + .modelRef( + new ModelRef("string") + ) + .parameterType("header") + .required(false) + .build() + ); + return new Docket(DocumentationType.SWAGGER_2) + .enable(enabled) + .apiInfo(apiInfo()) + .select() + .paths(Predicates.not(PathSelectors.regex("/error.*"))) + .build().directModelSubstitute(Timestamp.class, Date.class) + .globalOperationParameters(pars); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title(applicationName) + .version("1.0") + .build(); + } + +} + +/** + * 将Pageable转换展示在swagger中 + */ +@Configuration +class SwaggerDataConfig { + + @Bean + public AlternateTypeRuleConvention pageableConvention(final TypeResolver resolver) { + return new AlternateTypeRuleConvention() { + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public List rules() { + return newArrayList(newRule(resolver.resolve(Pageable.class), resolver.resolve(Page.class))); + } + }; + } + + @ApiModel + @Data + private static class Page { + @ApiModelProperty("页码 (0..N)") + private Integer page; + + @ApiModelProperty("每页显示的数目") + private Integer size; + + @ApiModelProperty("以下列格式排序标准:property[,asc | desc]。 默认排序顺序为升序。 支持多种排序条件:如:id,asc") + private List sort; + } +} diff --git a/dubhe-server/common-cloud/unit-test/pom.xml b/dubhe-server/common-cloud/unit-test/pom.xml new file mode 100644 index 0000000..d595b5a --- /dev/null +++ b/dubhe-server/common-cloud/unit-test/pom.xml @@ -0,0 +1,59 @@ + + + + org.dubhe.cloud + common-cloud + 0.0.1-SNAPSHOT + + 4.0.0 + + unit-test + 0.0.1-SNAPSHOT + unit-test 单元测试 + unit-test for Spring Cloud + + + org.springframework.boot + spring-boot-starter-test + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-context + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + + org.dubhe.biz + data-response + ${org.dubhe.biz.data-response.version} + + + + org.dubhe.cloud + auth-config + ${org.dubhe.cloud.auth-config.version} + + + + org.dubhe.cloud + remote-call + ${org.dubhe.cloud.remote-call.version} + + + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/base/BaseTest.java b/dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/base/BaseTest.java new file mode 100644 index 0000000..41786a9 --- /dev/null +++ b/dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/base/BaseTest.java @@ -0,0 +1,165 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.cloud.unittest.base; + +import cn.hutool.http.HttpStatus; +import org.dubhe.biz.base.constant.ApplicationNameConst; +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.cloud.unittest.config.UnitTestConfig; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.junit.Assert; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import java.io.UnsupportedEncodingException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @description 基础测试类 + * @date 2020-04-20 + */ +@ActiveProfiles(value = "dev") +@RunWith(SpringRunner.class) +@SpringBootTest +@EnableTransactionManagement +@WebAppConfiguration +public class BaseTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private FilterChainProxy springSecurityFilterChain; + + protected MockMvc mockMvc; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private UnitTestConfig unitTestConfig; + + public BaseTest() { + } + + /** + * 初始化MockMvc对象,添加Security过滤器链 + */ + @Before + public void setup() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).addFilter(springSecurityFilterChain).build(); + } + + /** + * mockMvcTest + * content 返回结果 + * status 返回状态码 + * + * @param mockHttpServletRequestBuilder 模拟HTTP请求 + * @param s 传入参数 + * @param ok 预期结果 + * @param i 预期状态码 + * @throws throws Exception + */ + public void mockMvcTest(MockHttpServletRequestBuilder mockHttpServletRequestBuilder, String s, ResultMatcher ok, int i) throws Exception { + String accessToken = obtainAccessToken(); + MockHttpServletResponse response = this.mockMvc.perform( + mockHttpServletRequestBuilder + .contentType(MediaType.APPLICATION_JSON) + .content(s).header(AuthConst.AUTHORIZATION, AuthConst.ACCESS_TOKEN_PREFIX + accessToken) + ).andExpect(ok) + .andReturn() + .getResponse(); + response.setCharacterEncoding("UTF-8"); + //得到返回状态码 + int status = response.getStatus(); + //得到返回结果 + String content = response.getContentAsString(); + //断言,判断返回代码是否正确 + Assert.assertEquals(i, status); + System.out.println(content); + } + + /** + * @param response 响应结果 + * @param i 响应码 + * @param @throws UnsupportedEncodingException 入参 + * @return void 返回类型 + * @throws @Title: mockMvcWithNoRequestBody + */ + public void mockMvcWithNoRequestBody(MockHttpServletResponse response, int i) throws UnsupportedEncodingException { + response.setCharacterEncoding("UTF-8"); + // 得到返回代码 + int status = response.getStatus(); + // 得到返回结果 + String content = response.getContentAsString(); + // 断言,判断返回代码是否正确 + Assert.assertEquals(i, status); + System.out.println("返回的参数" + content); + } + + /** + * 模拟登录获取token + * @return String token + */ + public String obtainAccessToken() { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", AuthConst.GRANT_TYPE); + params.add("client_id", AuthConst.CLIENT_ID); + params.add("client_secret", AuthConst.CLIENT_SECRET); + params.add("username", unitTestConfig.getUsername()); + params.add("password", unitTestConfig.getPassword()); + params.add("scope", "all"); + HttpHeaders headers = new HttpHeaders(); + // 需求需要传参为application/x-www-form-urlencoded格式 + headers.setContentType(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE)); + HttpEntity> httpEntity = new HttpEntity<>(params, headers); + ResponseEntity responseEntity = restTemplate.postForEntity("http://" + ApplicationNameConst.SERVER_AUTHORIZATION + "/oauth/token", httpEntity, DataResponseBody.class); + if (HttpStatus.HTTP_OK != responseEntity.getStatusCodeValue()) { + return null; + } + DataResponseBody restResult = responseEntity.getBody(); + Map map = new LinkedHashMap(); + if (restResult.succeed()) { + map = (Map) restResult.getData(); + } + // 返回 token + return (String) map.get("token"); + } +} diff --git a/dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/config/UnitTestConfig.java b/dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/config/UnitTestConfig.java new file mode 100644 index 0000000..7702caa --- /dev/null +++ b/dubhe-server/common-cloud/unit-test/src/main/java/org/dubhe/cloud/unittest/config/UnitTestConfig.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.cloud.unittest.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +/** + * @description 配置dev环境单元测试用户名与密码 + * @date 2021-01-11 + */ +@Data +@Component +@ConfigurationProperties(prefix = "unittest") +@RefreshScope +public class UnitTestConfig { + + private String username; + + private String password; + +} \ No newline at end of file diff --git a/dubhe-server/common-k8s/.gitignore b/dubhe-server/common-k8s/.gitignore new file mode 100644 index 0000000..3efc846 --- /dev/null +++ b/dubhe-server/common-k8s/.gitignore @@ -0,0 +1 @@ +!common-k8s/src/main/resources/kubeconfig diff --git a/dubhe-server/common-k8s/pom.xml b/dubhe-server/common-k8s/pom.xml new file mode 100644 index 0000000..5d49fe1 --- /dev/null +++ b/dubhe-server/common-k8s/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.dubhe + server + 0.0.1-SNAPSHOT + + common-k8s + 0.0.1-SNAPSHOT + k8s 通用模块 + Dubhe K8s Common + + + + + org.dubhe.biz + file + ${org.dubhe.biz.file.version} + + + org.dubhe.biz + redis + ${org.dubhe.biz.redis.version} + + + org.dubhe.biz + db + ${org.dubhe.biz.db.version} + + + + org.dubhe.cloud + swagger + ${org.dubhe.cloud.swagger.version} + + + + org.springframework.boot + spring-boot-starter-web + + + + + io.fabric8 + kubernetes-client + ${fabric.io.version} + + + + + com.google.code.gson + gson + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + + + org.elasticsearch + elasticsearch + + + + jakarta.validation + jakarta.validation-api + + + org.dubhe.biz + data-permission + 0.0.1-SNAPSHOT + compile + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + true + src/main/resources + + + + + diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/HarborApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/HarborApi.java new file mode 100644 index 0000000..2491c6b --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/HarborApi.java @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.harbor.api; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.harbor.domain.vo.ImageVO; +import java.util.List; +import java.util.Map; + +/** + * @description HarborApi接口 + * @date 2020-07-01 + */ +public interface HarborApi { + /** + * 通过ProjectName名称查询镜像名称 + * + * @param projectNameList 项目名称集合 + * @return List 镜像名称集合 + */ + List searchImageNames(List projectNameList); + /** + * 通过projectM名称查询镜像名称 + * + * @param projectNameList 项目名称集合 + * @return List 镜像结果集 + */ + List searchImageByProjects(List projectNameList); + + /** + * 根据完整镜像路径查询判断是否具有镜像 + * + * @param imageUrl 镜像路径 + * @return Boolean true 存在 false 不存在 + */ + Boolean isExistImage(String imageUrl); + /** + * 查询所有镜像名称不用根据ProjectName进行查询 + * + * @return List 镜像结果集 + **/ + List findImageList(); + /** + * 分页查询(镜像) + * + * @param page 分页对象 + * @return ImageVO 镜像vo对象 + **/ + ImageVO findImagePage(Page page); + /** + * 删除镜像 + * + * @param imageUrl 镜像的完整路径 + */ + void deleteImageByTag(String imageUrl); + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/impl/HarborApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/impl/HarborApiImpl.java new file mode 100644 index 0000000..d3b6824 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/api/impl/HarborApiImpl.java @@ -0,0 +1,312 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.harbor.api.impl; + + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.harbor.api.HarborApi; +import org.dubhe.harbor.domain.vo.ImagePageVO; +import org.dubhe.harbor.domain.vo.ImageVO; +import org.dubhe.harbor.utils.HttpClientUtils; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.CollectionUtils; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.dubhe.biz.base.constant.SymbolConstant.COLON; + +/** + * @description HarborApi接口实现类 + * @date 2020-05-21 + */ +public class HarborApiImpl implements HarborApi { + + @Value("https://${harbor.address}/api/repositories?project_id=") + private String imageSearchUrl; + @Value("${harbor.address}/") + private String imagePullUrl; + @Value("https://${harbor.address}/api/projects") + private String projectSearchUrl; + @Value("https://${harbor.address}/api/repositories/") + private String tagSearchUrl; + @Value("${harbor.username}") + private String harborName; + @Value("${harbor.password}") + private String harborPassword; + private static final String TAG_SEARCH_CONF = "/%2F"; + private static final String TAG_SEARCH_PARAMS = "/tags"; + private static final String RESOURCE_NAME_KEY = "name"; + private static final String PROJECT_ID_KEY = "project_id"; + private static final String CREATION_TIME = "creation_time"; + private static final String UPDATE_TIME = "update_time"; + private static final String CPL_IMAGE_NAMES = "cplImageName"; + + /** + * 通过ProjectName名称查询镜像名称 + * + * @param projectNameList 项目名称集合 + * @return List 镜像名称集合 + */ + @Override + public List searchImageNames(List projectNameList) { + List imageNames = new ArrayList<>(MagicNumConstant.TEN); + List imageInfoList = searchImageByProjects(projectNameList); + if (CollectionUtils.isEmpty(projectNameList) || CollectionUtils.isEmpty(imageInfoList)) { + return imageNames; + } + imageInfoList.forEach(imageInfo -> imageNames.add((String) imageInfo.get(CPL_IMAGE_NAMES))); + return imageNames; + } + + /** + * 通过projectM名称查询镜像名称 + * + * @param projectNameList 镜像名称 + * @return List 镜像结果集合 + */ + @Override + public List searchImageByProjects(List projectNameList) { + /**创建镜像名称List**/ + List imageInfoList = new ArrayList<>(MagicNumConstant.TEN); + /**处理数据非空判断**/ + if (CollectionUtils.isEmpty(projectNameList)) { + return imageInfoList; + } + /**获取projectName-projectId映射Map**/ + Map project = getProjectIdMap(); + projectNameList.parallelStream().forEach(projectName -> { + /**根据projectName获取projectId**/ + Integer projectId = project.get(projectName); + if (projectId != null) { + /**调用Harbor提供的http接口,传入projectId获取Json格式的repository 信息**/ + String repoJson = HttpClientUtils.sendHttps(imageSearchUrl + projectId); + /**解析repoJson**/ + JSONArray repoArray = JSON.parseArray(repoJson); + repoArray.stream().forEach(repo -> { + /**获取repoName**/ + Map repoDetailMap = (Map) repo; + String repoName = repoDetailMap.get(RESOURCE_NAME_KEY); + /**获取imageName**/ + String imageName = repoName.substring(projectName.length() + MagicNumConstant.ONE); + /**调用getImageTagNames方法获取镜像的tag**/ + List imageTagNames = getImageTagNameList(projectName, imageName); + /**获取creation_time**/ + String creationTime = repoDetailMap.get(CREATION_TIME); + /**获取update_time**/ + String updateTime = repoDetailMap.get(UPDATE_TIME); + imageTagNames.stream().forEach(imageTagName -> { + Map imageMap = new HashMap(MagicNumConstant.SIXTEEN); + imageMap.put("imageName", imageName); + imageMap.put("cplImageName", imagePullUrl + repoName + COLON + imageTagName); + imageMap.put("tag", imageTagName); + imageMap.put("creationTime", creationTime); + imageMap.put("updateTime", updateTime); + imageInfoList.add(imageMap); + }); + }); + } else { + LogUtil.error(LogEnum.BIZ_K8S, "The number information corresponding to the project name was not found {}", projectNameList); + } + }); + return imageInfoList; + } + + /** + * 获取projectName-projectId映射Map + * + * @return Map 项目名称和项目id映射map + */ + private Map getProjectIdMap() { + Map projectIdMap = new HashMap<>(MagicNumConstant.SIXTEEN); + /**调用Harbor提供的http接口,获取Json格式的project信息**/ + String projectJson = HttpClientUtils.sendHttps(projectSearchUrl); + /**解析projectArray**/ + JSONArray projectArray = JSON.parseArray(projectJson); + List jsonArrays = Arrays.asList(projectArray); + if (CollectionUtils.isEmpty(jsonArrays)) { + return projectIdMap; + } + JSONArray projects = jsonArrays.get(0); + /**将对象转换为Map集合**/ + projects.stream().forEach(project -> { + Map projectDetailMap = (Map) project; + /**得到projectName**/ + String projectName = (String) projectDetailMap.get(RESOURCE_NAME_KEY); + /**得到projectID**/ + Integer projectId = (Integer) projectDetailMap.get(PROJECT_ID_KEY); + projectIdMap.put(projectName, projectId); + }); + return projectIdMap; + } + + + /** + * 查询镜像Tag名称方法 + * + * @param projectName 项目名称 + * @param imageName 镜像名称 + * @return List 镜像标签集合名称 + */ + private List getImageTagNameList(String projectName, String imageName) { + List tagNames = new ArrayList<>(MagicNumConstant.TEN); + /***非空校验*/ + if (StringUtils.isBlank(projectName)) { + return tagNames; + } + if (StringUtils.isBlank(imageName)) { + return tagNames; + } + /**调用Harbor提供的http接口,获取Json格式的Tag信息**/ + String imageTagsJson = HttpClientUtils.sendHttps(tagSearchUrl + projectName + TAG_SEARCH_CONF + imageName + TAG_SEARCH_PARAMS); + if (StringUtils.isBlank(imageTagsJson)) { + return tagNames; + } + + /***解析imageTagsJson*/ + JSONArray tagsArray = JSON.parseArray(imageTagsJson); + /**遍历数据并且转换为map类型**/ + tagsArray.stream().forEach(tag -> { + /**获取tagName**/ + Map tagDetailMap = (Map) tag; + String tagName = tagDetailMap.get(RESOURCE_NAME_KEY); + tagNames.add(tagName); + }); + + return tagNames; + } + + + /** + * 根据完整镜像路径查询判断是否具有镜像 + * + * @param imageUrl 镜像路径 + * @return Boolean true 存在 false 不存在 + */ + @Override + public Boolean isExistImage(String imageUrl) { + if (StringUtils.isBlank(imageUrl)) { + LogUtil.info(LogEnum.BIZ_K8S, "The image path is empty", imageUrl); + return false; + } + /**将imageName(含版本号)分割出来**/ + String[] urlSplit = imageUrl.split(SymbolConstant.SLASH); + String imageName = urlSplit[MagicNumConstant.TWO]; + /**获取该项目下所有镜像名称**/ + List imageInfoList = searchImageByProjects(Arrays.asList(urlSplit[MagicNumConstant.ONE])); + /**创建集合存储该项目下面的所有的镜像**/ + List imageList = new ArrayList<>(MagicNumConstant.TEN); + /**根据ProjectName和image将版本号查询出来**/ + try { + if (!CollectionUtils.isEmpty(imageInfoList)) { + imageInfoList.stream().forEach(name -> { + /**将该项目下面的所有镜像存储进来**/ + imageList.add(((String) name.get(CPL_IMAGE_NAMES)).split(SymbolConstant.SLASH)[MagicNumConstant.TWO]); + }); + } + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_K8S, "error path:{}", imageUrl); + } + + return imageList.contains(imageName); + } + + /** + * 查询所有镜像名称不用根据ProjectName进行查询 + * + * @param + * @return List 镜像结果集 + **/ + @Override + public List findImageList() { + /**查询所有项目名称**/ + Map projectIdMap = getProjectIdMap(); + /**创建集合将项目名添加进去**/ + List projects = new ArrayList<>(projectIdMap.keySet()); + return searchImageByProjects(projects); + } + + /** + * 分页查询(镜像) + * + * @param page 分页对象 + * @return ImageVO 镜像vo对象 + **/ + @Override + public ImageVO findImagePage(Page page) { + /**查询所有项目名称**/ + Map projectIdMap = getProjectIdMap(); + /**创建集合将项目名添加进去**/ + List projects = new ArrayList<>(projectIdMap.keySet()); + List imageList = searchImageByProjects(projects); + ImageVO imageVO = new ImageVO(); + if (CollectionUtils.isEmpty(imageList)) { + List imagesData = imageList.stream().skip((page.getCurrent() - MagicNumConstant.ONE) * page.getSize()).limit(page.getSize()).collect(Collectors.toList()); + imageVO.setResult(imagesData); + ImagePageVO imagePageVO = new ImagePageVO(); + /**获取当前页**/ + imagePageVO.setCurrent(page.getCurrent()); + /**获取每页显示都数据**/ + imagePageVO.setSize(page.getSize()); + /**获取总记录数**/ + imagePageVO.setTotal(imageList.size()); + imageVO.setPage(imagePageVO); + } + return imageVO; + } + + /** + * 根据镜像标签删除镜像 + * + * @param imageUrl + */ + @Override + public void deleteImageByTag(String imageUrl) { + if(StringUtils.isNotEmpty(imageUrl)){ + LogUtil.info(LogEnum.BIZ_K8S,"image path{}",imageUrl); + String[] urlSplits = imageUrl.split(SymbolConstant.SLASH); + String[] tagUrls = urlSplits[MagicNumConstant.TWO].split(SymbolConstant.COLON); + String dataRep=urlSplits[MagicNumConstant.ONE]+SymbolConstant.SLASH+tagUrls[MagicNumConstant.ZERO]; + LogUtil.info(LogEnum.BIZ_K8S,"data warehouse{}",dataRep); + Map projectIdMap = getProjectIdMap(); + //获取harbor中所有项目的名称 + Set names = projectIdMap.keySet(); + //判断harbor中是否具有改项目 + names.forEach(name->{ + if(urlSplits[MagicNumConstant.ONE].equals(name)){ + //发送删除请求 + HttpClientUtils.sendHttpsDelete(tagSearchUrl+dataRep+TAG_SEARCH_PARAMS+SymbolConstant.SLASH+tagUrls[MagicNumConstant.ONE],harborName,harborPassword); + LogUtil.error(LogEnum.BIZ_K8S,"fail to delete{}",imageUrl); + return; + } + }); + } + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/config/HarborConfig.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/config/HarborConfig.java new file mode 100644 index 0000000..28c33af --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/config/HarborConfig.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.harbor.config; +import org.dubhe.harbor.api.impl.HarborApiImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @description Harbor spring bean 配置类 + * @date 2020-05-02 + */ + +@Configuration +public class HarborConfig { + @Bean + public HarborApiImpl harborApi(){ + return new HarborApiImpl(); + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImagePageVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImagePageVO.java new file mode 100644 index 0000000..18cf895 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImagePageVO.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.harbor.domain.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 镜像结果集 + * @date 2020-06-04 + */ +@Data +public class ImagePageVO implements Serializable { + private static final long serialVersionUID = 1L; + private Integer total; + private Long size; + private Long current; + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImageVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImageVO.java new file mode 100644 index 0000000..d31565c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/domain/vo/ImageVO.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.harbor.domain.vo; + +import lombok.Data; + + +import java.io.Serializable; +import java.util.List; + +/** + * @description 转换对象 + * @date 2020-06-04 + */ +@Data +public class ImageVO implements Serializable { + private static final long serialVersionUID = 1L; + private List result; + private ImagePageVO page; + + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/utils/HttpClientUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/utils/HttpClientUtils.java new file mode 100644 index 0000000..22ddfe3 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/harbor/utils/HttpClientUtils.java @@ -0,0 +1,169 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.harbor.utils; +import org.apache.commons.io.IOUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; + + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import org.apache.commons.codec.binary.Base64; +import static org.dubhe.biz.base.constant.StringConstant.UTF8; +import static org.dubhe.biz.base.constant.SymbolConstant.BLANK; + +/** + * @description httpClient工具类,不校验SSL证书 + * @date 2020-05-21 + */ +public class HttpClientUtils { + + public static String sendHttps(String path) { + InputStream inputStream = null; + BufferedReader bufferedReader = null; + InputStreamReader inputStreamReader = null; + StringBuilder stringBuilder = new StringBuilder(); + String result = BLANK; + HttpsURLConnection con = null; + try { + con = getConnection(path); + con.connect(); + + /**将返回的输入流转换成字符串**/ + inputStream = con.getInputStream(); + inputStreamReader = new InputStreamReader(inputStream, UTF8); + bufferedReader = new BufferedReader(inputStreamReader); + + String str = null; + while ((str = bufferedReader.readLine()) != null) { + stringBuilder.append(str); + } + + result = stringBuilder.toString(); + LogUtil.info(LogEnum.BIZ_SYS,"Request path:{}, SUCCESS, result:{}", path, result); + + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_SYS,"Request path:{}, ERROR, exception:{}", path, e); + return result; + } finally { + closeResource(bufferedReader,inputStreamReader,inputStream,con); + } + + return result; + } + public static String sendHttpsDelete(String path,String username,String password) { + InputStream inputStream = null; + BufferedReader bufferedReader = null; + InputStreamReader inputStreamReader = null; + StringBuilder stringBuilder = new StringBuilder(); + String result = BLANK; + HttpsURLConnection con = null; + try { + con = getConnection(path); + String input =username+ ":" +password; + String encoding=Base64.encodeBase64String(input.getBytes()); + con.setRequestProperty("Authorization", "Basic " + encoding); + con.setRequestMethod("DELETE"); + con.connect(); + /**将返回的输入流转换成字符串**/ + inputStream = con.getInputStream(); + inputStreamReader = new InputStreamReader(inputStream, UTF8); + bufferedReader = new BufferedReader(inputStreamReader); + + String str = null; + while ((str = bufferedReader.readLine()) != null) { + stringBuilder.append(str); + } + + result = stringBuilder.toString(); + LogUtil.info(LogEnum.BIZ_SYS,"Request path:{}, SUCCESS, result:{}", path, result); + + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_SYS,"Request path:{}, ERROR, exception:{}", path, e); + return result; + } finally { + closeResource(bufferedReader,inputStreamReader,inputStream,con); + } + + return result; + } + + private static void closeResource(BufferedReader bufferedReader,InputStreamReader inputStreamReader,InputStream inputStream,HttpsURLConnection con) { + if (inputStream != null) { + IOUtils.closeQuietly(inputStream); + } + + if (inputStreamReader != null) { + IOUtils.closeQuietly(inputStreamReader); + } + if (bufferedReader != null) { + IOUtils.closeQuietly(bufferedReader); + } + if (con != null) { + con.disconnect(); + } + } + + + private static HttpsURLConnection getConnection(String path){ + HttpsURLConnection con = null; + try { + /**创建并初始化SSLContext对象**/ + TrustManager[] trustManagers = {new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }}; + SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); + sslContext.init(null, trustManagers, new java.security.SecureRandom()); + + /**得到SSLSocketFactory对象**/ + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + URL url = new URL(path); + con = (HttpsURLConnection) url.openConnection(); + con.setSSLSocketFactory(sslSocketFactory); + con.setUseCaches(false); + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_SYS,"Request path:{}, error, exception:{}", path, e); + } + return con; + + } + +} + + diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractDeploymentCallback.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractDeploymentCallback.java new file mode 100644 index 0000000..2dd32fe --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractDeploymentCallback.java @@ -0,0 +1,85 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.abstracts; + +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.domain.dto.BaseK8sDeploymentCallbackCreateDTO; +import org.dubhe.k8s.service.DeploymentCallbackAsyncService; +import org.dubhe.k8s.utils.K8sCallBackTool; + +import javax.annotation.Resource; + +/** + * @description 公共 deployment回调 + * @date 2020-11-27 + */ +public abstract class AbstractDeploymentCallback implements DeploymentCallbackAsyncService { + + @Resource + private K8sCallBackTool k8sCallBackTool; + + /** + * 公共 失败重试策略 + * + * @param k8sDeploymentCallbackCreateDTO + * @param + */ + @Override + public void deploymentCallBack(R k8sDeploymentCallbackCreateDTO) { + int tryTime = 1; + while (!doCallback(tryTime, k8sDeploymentCallbackCreateDTO)) { + if (k8sCallBackTool.continueRetry(++tryTime)) { + // 继续重试 tryTime重试次数+1 + try { + Thread.sleep(tryTime * NumberConstant.NUMBER_1000); + } catch (InterruptedException e) { + LogUtil.error(LogEnum.NOTE_BOOK, "AbstractDeploymentCallback deploymentCallBack InterruptedException : {}", e); + // Restore interrupted state...       + Thread.currentThread().interrupt(); + } + } else { + // 重试超限 tryTime重试次数+1未尝试,因此需要tryTime重试次数-1 + callbackFailed(--tryTime, k8sDeploymentCallbackCreateDTO); + break; + } + } + } + + + /** + * deployment 异步回调具体实现处理类 + * + * @param times 第n次处理 + * @param k8sDeploymentCallbackCreateDTO k8s回调实体类 + * @param BaseK8sDeploymentCallbackReq k8s回调基类 + * @return true:处理成功 false:处理失败 + */ + public abstract boolean doCallback(int times, R k8sDeploymentCallbackCreateDTO); + + + /** + * deployment 异步回调具体实现处理类 + * + * @param retryTimes 总处理次数 + * @param k8sDeploymentCallbackCreateDTO k8s回调实体类 + * @param BaseK8sDeploymentCallbackReq k8s回调基类 + */ + public abstract void callbackFailed(int retryTimes, R k8sDeploymentCallbackCreateDTO); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractPodCallback.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractPodCallback.java new file mode 100644 index 0000000..2f022f7 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/abstracts/AbstractPodCallback.java @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.abstracts; + + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.domain.dto.BaseK8sPodCallbackCreateDTO; +import org.dubhe.k8s.service.PodCallbackAsyncService; +import org.dubhe.k8s.utils.K8sCallBackTool; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @description pod 异步回调抽象处理类 + * @date 2020-05-29 + */ +public abstract class AbstractPodCallback implements PodCallbackAsyncService { + + @Autowired + private K8sCallBackTool k8sCallBackTool; + + /** + * 公共 失败重试策略 + * + * @param k8sPodCallbackCreateDTO + * @param + */ + @Override + public void podCallBack(R k8sPodCallbackCreateDTO) { + int tryTime = 1; + while (!doCallback(tryTime, k8sPodCallbackCreateDTO)) { + if (k8sCallBackTool.continueRetry(++tryTime)) { + // 继续重试 tryTime重试次数+1 + try { + Thread.sleep(tryTime * 1000); + continue; + } catch (InterruptedException e) { + LogUtil.error(LogEnum.NOTE_BOOK, "AbstractPodCallback podCallBack InterruptedException : {}", e); + // Restore interrupted state...       + Thread.currentThread().interrupt(); + } + } else { + // 重试超限 tryTime重试次数+1未尝试,因此需要tryTime重试次数-1 + callbackFailed(--tryTime, k8sPodCallbackCreateDTO); + break; + } + } + } + + /** + * pod 异步回调具体实现处理类 + * @param times 第n次处理 + * @param k8sPodCallbackCreateDTO k8s回调实体类 + * @param BaseK8sPodCallbackReq k8s回调基类 + * @return true:处理成功 false:处理失败 + */ + public abstract boolean doCallback(int times, R k8sPodCallbackCreateDTO); + + + /** + * pod 异步回调具体实现处理类 + * @param retryTimes 总处理次数 + * @param k8sPodCallbackCreateDTO k8s回调实体类 + * @param BaseK8sPodCallbackReq k8s回调基类 + */ + public abstract void callbackFailed(int retryTimes, R k8sPodCallbackCreateDTO); + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sField.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sField.java new file mode 100644 index 0000000..cfa2615 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sField.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.annotation; + + +import org.dubhe.biz.base.constant.SymbolConstant; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @description 用于转换fabric pojo的自定义注解 + * @date 2020-04-14 + */ + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface K8sField { + String value() default SymbolConstant.BLANK; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sValidation.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sValidation.java new file mode 100644 index 0000000..c7017f4 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/annotation/K8sValidation.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.annotation; + +import org.dubhe.k8s.enums.ValidationTypeEnum; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @description 用于标记需要校验的字段 + * @date 2020-06-04 + */ +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface K8sValidation { + ValidationTypeEnum value(); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DistributeTrainApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DistributeTrainApi.java new file mode 100644 index 0000000..c4ce7c0 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DistributeTrainApi.java @@ -0,0 +1,68 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.DistributeTrainBO; +import org.dubhe.k8s.domain.resource.BizDistributeTrain; + +/** + * @description k8s中资源为DistributeTrain的操作接口 + * @date 2020-07-07 + */ +public interface DistributeTrainApi { + BizDistributeTrain create(DistributeTrainBO bo); + + /** + * 删除 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult + */ + PtBaseResult deleteByResourceName(String namespace, String resourceName); + + /** + * 根据namespace和resourceName查询cr信息 + * + * @param namespce 命令空间 + * @param resourceName 资源名称 + * @return BizDistributeTrain 自定义资源转换类集合 + */ + BizDistributeTrain get(String namespce,String resourceName); + /** + * 根据名称查cr + * + * @param crName 自定义资源名称 + * @return BizDistributeTrain 自定义资源类 + */ + BizDistributeTrain findDisByName(String crName); + + /** + * 通过 yaml创建 + * @param crYaml cr定义yaml脚本 + * @return BizDistributeTrain 自定义资源类 + */ + BizDistributeTrain create(String crYaml); + + /** + * 通过 yaml删除 + * @param crYaml cr定义yaml脚本 + * @return boolean true 删除成功 false删除失败 + */ + boolean delete(String crYaml); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DubheDeploymentApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DubheDeploymentApi.java new file mode 100644 index 0000000..4f4a0db --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/DubheDeploymentApi.java @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtModelOptimizationDeploymentBO; +import org.dubhe.k8s.domain.resource.BizDeployment; + +import java.util.List; + +/** + * @description k8s中资源为Deployment的操作接口 + * @date 2020-07-03 + */ +public interface DubheDeploymentApi { + /** + * 创建模型压缩Deployment + * + * @param bo 模型压缩 Deployment BO + * @return BizDeployment Deployment 业务类 + */ + BizDeployment create(PtModelOptimizationDeploymentBO bo); + + /** + * 通过命名空间和资源名称查找Deployment资源 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizDeployment Deployment 业务类 + */ + BizDeployment getWithResourceName(String namespace,String resourceName); + + /** + * 通过命名空间查找Deployment资源集合 + * + * @param namespace 命名空间 + * @return List Deployment 业务类集合 + */ + List getWithNamespace(String namespace); + + /** + * 查询集群所有Deployment资源 + * + * @return List Deployment 业务类集合 + */ + List listAll(); + + /** + * 通过资源名进行删除 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult deleteByResourceName(String namespace, String resourceName); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/JupyterResourceApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/JupyterResourceApi.java new file mode 100644 index 0000000..230fa43 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/JupyterResourceApi.java @@ -0,0 +1,73 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtJupyterResourceBO; +import org.dubhe.k8s.domain.vo.PtJupyterDeployVO; + +import java.util.List; + +/** + * @description Jupyter Notebook 操作接口 + * @date 2020-07-03 + */ +public interface JupyterResourceApi { + + /** + * 创建Notebook Deployment + * + * @param bo 模型管理 Notebook BO + * @return PtJupyterDeployVO Notebook 结果类 + */ + PtJupyterDeployVO create(PtJupyterResourceBO bo); + + /** + * 创建Notebook Deployment并使用pvc存储 + * + * @param bo 模型管理 Notebook BO + * @return PtJupyterDeployVO Notebook 结果类 + */ + PtJupyterDeployVO createWithPvc(PtJupyterResourceBO bo); + + /** + * 删除Notebook Deployment + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult delete(String namespace, String resourceName); + + /** + * 查询命名空间下所有Notebook + * + * @param namespace 命名空间 + * @return List Notebook 结果类集合 + */ + List list(String namespace); + + /** + * 查询Notebook + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtJupyterDeployVO Notebook 结果类 + */ + PtJupyterDeployVO get(String namespace, String resourceName); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LimitRangeApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LimitRangeApi.java new file mode 100644 index 0000000..c62f5f3 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LimitRangeApi.java @@ -0,0 +1,56 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtLimitRangeBO; +import org.dubhe.k8s.domain.resource.BizLimitRange; + +import java.util.List; + +/** + * @description 限制命名空间下Pod的资源配额 + * @date 2020-07-03 + */ +public interface LimitRangeApi { + /** + * 创建LimitRange + * + * @param bo LimitRange BO + * @return BizLimitRange LimitRange 业务类 + */ + BizLimitRange create(PtLimitRangeBO bo); + + /** + * 查询命名空间下所有LimitRange + * + * @param namespace 命名空间 + * @return List LimitRange 业务类集合 + */ + List list(String namespace); + + /** + * 删除LimitRange + * + * @param namespace 命名空间 + * @param name LimitRange 名称 + * @return PtBaseResult 基本结果类 + */ + PtBaseResult delete(String namespace, String name); + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LogMonitoringApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LogMonitoringApi.java new file mode 100644 index 0000000..20d9bd0 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/LogMonitoringApi.java @@ -0,0 +1,78 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; +import org.dubhe.k8s.domain.bo.LogMonitoringBO; +import org.dubhe.k8s.domain.vo.LogMonitoringVO; + +import java.util.List; + + +/** + * @description 日志监控接口 + * @date 2020-07-03 + */ +public interface LogMonitoringApi { + + /** + * 添加Pod日志到ES,无日志参数,默认从k8s集群查询日志添加到ES + * + * @param podName Pod名称 + * @param namespace 命名空间 + * @return boolean 日志添加是否成功 + */ + boolean addLogsToEs(String podName,String namespace); + + /** + * 添加Pod自定义日志到ES + * + * @param podName Pod名称 + * @param namespace 命名空间 + * @param logList 日志信息 + * @return boolean 日志添加是否成功 + */ + boolean addLogsToEs(String podName, String namespace, List logList); + + /** + * 日志查询方法 + * + * @param from 日志查询起始值,初始值为1,表示从第一条日志记录开始查询 + * @param size 日志查询记录数 + * @param logMonitoringBo 日志查询bo + * @return LogMonitoringVO 日志查询结果类 + */ + LogMonitoringVO searchLogByResName(int from, int size, LogMonitoringBO logMonitoringBo); + + /** + * 日志查询方法 + * + * @param from 日志查询起始值,初始值为1,表示从第一条日志记录开始查询 + * @param size 日志查询记录数 + * @param logMonitoringBo 日志查询bo + * @return LogMonitoringVO 日志查询结果类 + */ + LogMonitoringVO searchLogByPodName(int from, int size, LogMonitoringBO logMonitoringBo); + + /** + * Pod 日志总量查询方法 + * + * @param logMonitoringBo 日志查询bo + * @return long Pod 产生的日志总量 + */ + long searchLogCountByPodName(LogMonitoringBO logMonitoringBo); + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/MetricsApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/MetricsApi.java new file mode 100644 index 0000000..d16c272 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/MetricsApi.java @@ -0,0 +1,107 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.dto.PodQueryDTO; +import org.dubhe.k8s.domain.vo.PodRangeMetricsVO; +import org.dubhe.k8s.domain.vo.PtContainerMetricsVO; +import org.dubhe.k8s.domain.vo.PtNodeMetricsVO; +import org.dubhe.k8s.domain.vo.PtPodsVO; + +import java.util.List; + +/** + * @description 监控信息查询接口 + * @date 2020-07-03 + */ +public interface MetricsApi { + /** + * 获取k8s所有节点当前cpu、内存用量 + * + * @return List NodeMetrics 结果类集合 + */ + List getNodeMetrics(); + + /** + * 获取k8s所有Pod资源用量的实时信息 + * + * @return List Pod资源用量结果类集合 + */ + List getPodsMetricsRealTime(); + + /** + * 获取k8s所有pod当前cpu、内存用量的实时使用情况 + * + * @return List Pod资源用量结果类集合 + */ + List getPodMetricsRealTime(); + + /** + * 获取k8s resourceName 下pod当前cpu、内存用量的实时使用情况 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return List Pod资源用量结果类集合 + */ + List getPodMetricsRealTime(String namespace,String resourceName); + + /** + * 获取k8s pod当前cpu、内存用量的实时使用情况 + * @param namespace 命名空间 + * @param podName pod名称 + * @return List Pod资源用量结果类集合 + */ + List getPodMetricsRealTimeByPodName(String namespace,String podName); + + /** + * 获取k8s pod当前cpu、内存用量的实时使用情况 + * @param namespace 命名空间 + * @param podNames pod名称列表 + * @return List Pod资源用量结果类集合 + */ + List getPodMetricsRealTimeByPodName(String namespace,List podNames); + + /** + * 获取k8s resourceName 下pod 时间范围内cpu、内存用量的实时使用情况 + * @param podQueryDTO Pod基础信息查询入参 + * @return List Pod监控指标 列表 + */ + List getPodRangeMetrics(PodQueryDTO podQueryDTO); + + /** + * 根据namespace、podName获取k8s pod 时间范围内cpu、内存用量的实时使用情况 + * @param podQueryDTO Pod基础信息查询入参 + * @return List Pod监控指标 列表 + */ + List getPodRangeMetricsByPodName(PodQueryDTO podQueryDTO); + + /** + * 查询命名空间下所有Pod的cpu和内存使用 + * + * @param namespace 命名空间 + * @return List Pod资源用量结果类集合 + */ + List getContainerMetrics(String namespace); + + /** + * 查询所有命名空间下所有pod的cpu和内存使用 + * + * @return List Pod资源用量结果类集合 + */ + List getContainerMetrics(); + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelOptJobApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelOptJobApi.java new file mode 100644 index 0000000..0e500ef --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelOptJobApi.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtModelOptimizationJobBO; +import org.dubhe.k8s.domain.resource.BizJob; + +import java.util.List; + +/** + * @description 模型压缩操作接口 + * @date 2020-07-03 + */ +public interface ModelOptJobApi { + /** + * 创建模型优化 Job + * + * @param bo 模型优化 Job BO + * @return BizJob Job业务类 + */ + BizJob create(PtModelOptimizationJobBO bo); + + /** + * 通过命名空间和资源名称查找Job资源 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizJob Job 业务类 + */ + BizJob getWithResourceName(String namespace,String resourceName); + + /** + * 通过命名空间查找Job资源 + * + * @param namespace 命名空间 + * @return List Job 业务类集合 + */ + List getWithNamespace(String namespace); + + /** + * 查询所有Job资源 + * + * @return List Job 业务类集合 + */ + List listAll(); + + /** + * 通过命名空间和资源名删除Job + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult deleteByResourceName(String namespace, String resourceName); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelServingApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelServingApi.java new file mode 100644 index 0000000..a12ee64 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ModelServingApi.java @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.ModelServingBO; +import org.dubhe.k8s.domain.vo.ModelServingVO; + +/** + * @description 模型部署接口 + * @date 2020-09-09 + */ +public interface ModelServingApi { + + /** + * 创建 + * @param bo + * @return + */ + ModelServingVO create(ModelServingBO bo); + + /** + * 删除 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult delete(String namespace, String resourceName); + + /** + * 查询 + * @param namespace + * @param resourceName + * @return + */ + ModelServingVO get(String namespace, String resourceName); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NamespaceApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NamespaceApi.java new file mode 100644 index 0000000..11e60a1 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NamespaceApi.java @@ -0,0 +1,145 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import io.fabric8.kubernetes.api.model.ResourceQuota; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.resource.BizNamespace; +import org.dubhe.k8s.enums.ValidationTypeEnum; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description k8s中资源为命名空间操作接口 + * @date 2020-07-03 + */ +public interface NamespaceApi { + /** + * 创建NamespaceLabels,为null则不添加标签 + * + * @param namespace 命名空间 + * @param labels 标签Map + * @return BizNamespace Namespace 业务类 + */ + BizNamespace create(@K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) String namespace, Map labels); + + /** + * 根据namespace查询BizNamespace + * + * @param namespace 命名空间 + * @return BizNamespace Namespace 业务类 + */ + BizNamespace get(String namespace); + + /** + * 查询所有的BizNamespace + * + * @return List Namespace 业务类集合 + */ + List listAll(); + + /** + * 根据label标签查询所有的BizNamespace + * + * @param labelKey 标签的键 + * @return List Namespace 业务类集合 + */ + List list(String labelKey); + + /** + * 根据label标签集合查询所有的BizNamespace数据 + * + * @param labels 标签键的集合 + * @return List Namespace业务类集合 + */ + List list(Set labels); + + /** + * 删除命名空间 + * + * @param namespace 命名空间 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult delete(String namespace); + + /** + * 删除命名空间的标签 + * + * @param namespace 命名空间 + * @param labelKey 标签的键 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult removeLabel(String namespace, String labelKey); + + /** + * 删除命名空间下的多个标签 + * + * @param namespace 命名空间 + * @param labels 标签键的集合 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult removeLabels(String namespace, Set labels); + + /** + * 将labelKey和labelValue添加到指定的命名空间 + * + * @param namespace 命名空间 + * @param labelKey 标签的键 + * @param labelValue 标签的值 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult addLabel(String namespace, String labelKey, String labelValue); + + /** + * 将多个label标签添加到指定的命名空间 + * + * @param namespace 命名空间 + * @param labels 标签 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult addLabels(String namespace, Map labels); + + /** + * 命名空间的资源限制 + * + * @param namespace 命名空间 + * @param quota 资源限制参数类 + * @return ResourceQuota 资源限制参数类 + */ + ResourceQuota addResourceQuota(String namespace, ResourceQuota quota); + + /** + * 获得命名空间下的所有的资源限制 + * + * @param namespace 命名空间 + * @return List 资源限制参数类 + */ + List listResourceQuotas(String namespace); + + /** + * 解除对命名空间的资源限制 + * + * @param namespace 命名空间 + * @param quota 资源限制参数类 + * @return boolean true删除成功 false删除失败 + */ + boolean removeResourceQuota(String namespace, ResourceQuota quota); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NativeResourceApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NativeResourceApi.java new file mode 100644 index 0000000..2e68224 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NativeResourceApi.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.api; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +import java.util.List; + +/** + * @description Kubernetes 原生资源对象操作 + * @date 2020-08-28 + */ +public interface NativeResourceApi { + /** + * 通过 yaml创建 + * @param crYaml cr定义yaml脚本 + * @return List + */ + List create(String crYaml); + + /** + * 通过 yaml删除 + * @param crYaml cr定义yaml脚本 + * @return boolean true删除成功 false删除失败 + */ + boolean delete(String crYaml); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NodeApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NodeApi.java new file mode 100644 index 0000000..d2b531a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/NodeApi.java @@ -0,0 +1,210 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import io.fabric8.kubernetes.api.model.Toleration; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.resource.BizNode; +import org.dubhe.k8s.domain.resource.BizTaint; +import org.dubhe.k8s.enums.LackOfResourcesEnum; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description Node(k8s节点)操作接口 + * @date 2020-07-03 + */ +public interface NodeApi { + /** + * 根据节点名称查询节点信息 + * + * @param nodeName 节点名称 + * @return BizNode Node 业务类 + */ + BizNode get(String nodeName); + + /** + * 查询所有节点信息 + * + * @return List Node 业务类集合 + */ + List listAll(); + + /** + * 给节点添加单个标签 + * + * @param nodeName 节点名称 + * @param labelKey 标签的键 + * @param labelValue 标签的值 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult addLabel(String nodeName, String labelKey, String labelValue); + + /** + * 给节点添加多个标签 + * + * @param nodeName 节点名称 + * @param labels 标签Map + * @return PtBaseResult 基础结果类 + */ + PtBaseResult addLabels(String nodeName, Map labels); + + /** + * 删除节点单个标签 + * + * @param nodeName 节点名称 + * @param labelKey 标签的键 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult deleteLabel(String nodeName, String labelKey); + + /** + * 删除节点的多个标签 + * + * @param nodeName 节点名称 + * @param labels 标签键集合 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult deleteLabels(String nodeName, Set labels); + + /** + * 根据标签查询节点 + * + * @param key 标签key + * @param value 标签value + * @return List + */ + List getWithLabel(String key,String value); + + /** + * 根据标签查询节点 + * + * @param labels 标签 + * @return List + */ + List getWithLabels(Map labels); + + /** + * 设置节点是否可调度 + * + * @param nodeName 节点名称 + * @param schedulable 参数true或false + * @return PtBaseResult 基础结果类 + */ + PtBaseResult schedulable(String nodeName, boolean schedulable); + + /** + * 查询集群资源是否充足 + * + * @param nodeSelector 节点选择标签 + * @param taints 该资源所能容忍的污点 + * @param cpuNum 单位为m 1核等于1000m + * @param memNum 单位为Mi 1Mi等于1024Ki + * @param gpuNum 单位为显卡,即"1"表示1张显卡 + * @return LackOfResourcesEnum 资源缺乏枚举类 + */ + LackOfResourcesEnum isAllocatable(Map nodeSelector, List taints, Integer cpuNum, Integer memNum, Integer gpuNum); + + /** + * 查询集群资源是否充足 + * + * @param cpuNum 单位为m 1核等于1000m + * @param memNum 单位为Mi 1Mi等于1024Ki + * @param gpuNum 单位为显卡,即"1"表示1张显卡 + * @return LackOfResourcesEnum 资源缺乏枚举类 + */ + LackOfResourcesEnum isAllocatable(Integer cpuNum, Integer memNum, Integer gpuNum); + + /** + * 判断是否超出总可分配gpu数 + * @param gpuNum + * @return LackOfResourcesEnum 资源缺乏枚举类 + */ + LackOfResourcesEnum isOutOfTotalAllocatableGpu(Integer gpuNum); + + /** + * 添加污点 + * + * @param nodeName 节点名称 + * @param bizTaintList 污点 + * @return BizNode + */ + BizNode taint(String nodeName, List bizTaintList); + + /** + * 删除污点 + * + * @param nodeName 节点名称 + * @param bizTaintList 污点 + * @return BizNode + */ + BizNode delTaint(String nodeName, List bizTaintList); + + /** + * 删除污点 + * + * @param nodeName 节点名称 + * @return BizNode + */ + BizNode delTaint(String nodeName); + + /** + * 根据id获取 node资源隔离 标志 + * + * @param isolationId + * @return node资源隔离 标志 + */ + String getNodeIsolationValue(Long isolationId); + + /** + * 获取当前用户 + * + * @return node资源隔离 标志 + */ + String getNodeIsolationValue(); + + /** + * 获取当前用户资源隔离 Toleration + * + * @return Toleration + */ + Toleration getNodeIsolationToleration(); + + /** + * 获取当前用户 资源隔离 NodeSelector + * @return Map + */ + Map getNodeIsolationNodeSelector(); + + /** + * 根据userid 生成 BizTaint 列表 + * + * @param userId + * @return + */ + List geBizTaintListByUserId(Long userId); + + /** + * 根据当前用户 生成 BizTaint 列表 + * + * @return + */ + List geBizTaintListByUserId(); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PersistentVolumeClaimApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PersistentVolumeClaimApi.java new file mode 100644 index 0000000..cbcdf0a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PersistentVolumeClaimApi.java @@ -0,0 +1,113 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import io.fabric8.kubernetes.api.model.PersistentVolume; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtPersistentVolumeClaimBO; +import org.dubhe.k8s.domain.resource.BizPersistentVolumeClaim; + +import java.util.List; + +/** + * @description k8s中资源为持久卷声明操作接口 + * @date 2020-07-03 + */ +public interface PersistentVolumeClaimApi { + /** + * 创建PVC + * + * @param bo PVC BO + * @return BizPersistentVolumeClaim PVC业务类 + */ + BizPersistentVolumeClaim create(PtPersistentVolumeClaimBO bo); + + /** + * 创建挂载文件存储服务 PV + * + * @param bo PVC bo + * @return BizPersistentVolumeClaim PVC业务类 + */ + BizPersistentVolumeClaim createWithFsPv(PtPersistentVolumeClaimBO bo); + + /** + * 创建挂载直接存储 PV + * + * @param bo PVC BO + * @return BizPersistentVolumeClaim PVC业务类 + */ + BizPersistentVolumeClaim createWithDirectPv(PtPersistentVolumeClaimBO bo); + + /** + * 创建创建自动挂载nfs动态存储的 PVC + * + * @param bo PVC bo + * @return BizPersistentVolumeClaim PVC 业务类 + */ + BizPersistentVolumeClaim createDynamicNfs(PtPersistentVolumeClaimBO bo); + + /** + * 查询命名空间下所有PVC + * + * @param namespace 命名空间 + * @return List PVC 业务类集合 + */ + List list(String namespace); + + /** + * 回收存储(recycle 的pv才能回收) + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult recycle(String namespace, String resourceName); + + /** + * 删除具体的PVC + * + * @param namespace 命名空间 + * @param pvcName PVC名称 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult delete(String namespace, String pvcName); + + /** + * 拼接storageClassName + * + * @param pvcName PVC 名称 + * @return String storageClassName + */ + String getStorageClassName(String pvcName); + + /** + * 删除PV + * + * @param pvName PV 名称 + * @return boolean true删除成功 false删除失败 + */ + boolean deletePv(String pvName); + + /** + * 查询PV + * + * @param pvName PV 名称 + * @return PersistentVolume PV 实体类 + */ + PersistentVolume getPv(String pvName); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PodApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PodApi.java new file mode 100644 index 0000000..86b589a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/PodApi.java @@ -0,0 +1,173 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import io.fabric8.kubernetes.api.model.Pod; +import org.dubhe.k8s.domain.bo.LabelBO; +import org.dubhe.k8s.domain.resource.BizPod; +import org.dubhe.k8s.domain.vo.PtPodsVO; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description k8s中资源为Pod的操作接口 + * @date 2020-07-03 + */ +public interface PodApi { + + /** + * 根据Pod名称和命名空间查询Pod + * + * @param namespace 命名空间 + * @param podName Pod名称 + * @return BizPod Pod业务类 + */ + BizPod get(String namespace, String podName); + + /** + * 根据Pod名称列表和命名空间查询Pod列表 + * + * @param namespace 命名空间 + * @param podNames Pod名称 + * @return List Pod业务类列表 + */ + List get(String namespace, List podNames); + /** + * 根据命名空间和资源名查询Pod + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizPod Pod业务类 + */ + + BizPod getWithResourceName(String namespace, String resourceName); + /** + * 根据命名空间和资源名查询Pod集合 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return List Pod业务类集合 + */ + List getListByResourceName(String namespace, String resourceName); + + /** + * 查询命名空间下所有Pod + * + * @param namespace 命名空间 + * @return List Pod业务类集合 + */ + List getWithNamespace(String namespace); + + /** + * 查询集群所有Pod + * + * @return List Pod业务类集合 + */ + List listAll(); + + List findByDtName(String dtName); + /** + * 根据Node分组获得所有运行中的Pod + * + * @return Map> 键为Node名称,值为Pod业务类集合 + */ + Map> listAllRuningPodGroupByNodeName(); + + /** + * 根据Node分组获取Pod信息 + * + * @return Map> 键为Node名称,值为Pod结果类集合 + */ + Map> getPods(); + + /** + * 根据label查询Pod集合 + * + * @param labelBO k8s label资源 bo + * @return List Pod 实体类集合 + */ + List list(LabelBO labelBO); + + /** + * 根据多个label查询Pod集合 + * + * @param labelBos label资源 bo 的集合 + * @return List Pod 实体类集合 + */ + List list(Set labelBos); + + /** + * 根据命名空间查询Pod集合 + * + * @param namespace 命名空间 + * @return List Pod 实体类集合 + */ + List list(String namespace); + + /** + * 根据命名空间和label查询Pod集合 + * + * @param namespace 命名空间 + * @param labelBO label资源 bo + * @return List Pod 实体类集合 + */ + List list(String namespace, LabelBO labelBO); + + /** + * 根据命名空间和多个label查询Pod集合 + * + * @param namespace 命名空间 + * @param labelBos label资源 bo 的集合 + * @return List Pod 实体类集合 + */ + List list(String namespace, Set labelBos); + + + /** + * 根据命名空间和Pod名称查询Token信息 + * + * @param namespace 命名空间 + * @param podName Pod名称 + * @return String token + */ + String getToken(String namespace, String podName); + + /** + * 根据命名空间和资源名获得Token信息 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return String token + */ + String getTokenByResourceName(String namespace, String resourceName); + + /** + * 根据命名空间和资源名查询Notebook url + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return String validateJupyterUrl 验证Jupyte的url值 + */ + String getUrlByResourceName(String namespace, String resourceName); + + + + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceIisolationApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceIisolationApi.java new file mode 100644 index 0000000..1740e0f --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceIisolationApi.java @@ -0,0 +1,57 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.api.model.batch.Job; +import org.dubhe.k8s.domain.cr.DistributeTrain; + +/** + * @description 资源隔离接口 + * @date 2021-05-20 + */ +public interface ResourceIisolationApi { + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param job + */ + void addIisolationInfo(Job job); + + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param deployment + */ + void addIisolationInfo(Deployment deployment); + + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param statefulSet + */ + void addIisolationInfo(StatefulSet statefulSet); + + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param distributeTrain + */ + void addIisolationInfo(DistributeTrain distributeTrain); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceQuotaApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceQuotaApi.java new file mode 100644 index 0000000..278903d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/ResourceQuotaApi.java @@ -0,0 +1,77 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtResourceQuotaBO; +import org.dubhe.k8s.domain.resource.BizResourceQuota; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; + +import java.util.List; + +/** + * @description 限制命名空间整体的资源配额 + * @date 2020-07-03 + */ +public interface ResourceQuotaApi { + /** + * 创建 ResourceQuota + * + * @param bo ResourceQuota BO + * @return BizResourceQuota ResourceQuota 业务类 + */ + BizResourceQuota create(PtResourceQuotaBO bo); + + /** + * 创建 ResourceQuota + * @param namespace 命名空间 + * @param name ResourceQuota 名称 + * @param cpu cpu限制 单位核 0表示不限制 + * @param memory 内存限制 单位G 0表示不限制 + * @param gpu gpu限制 单位张 0表示不限制 + * @return + */ + BizResourceQuota create(String namespace,String name,Integer cpu,Integer memory,Integer gpu); + + /** + * 根据命名空间查询ResourceQuota集合 + * + * @param namespace 命名空间 + * @return List ResourceQuota 业务类集合 + */ + List list(String namespace); + + /** + * 删除ResourceQuota + * + * @param namespace 命名空间 + * @param name ResourceQuota 名称 + * @return PtBaseResult 基础结果类 + */ + PtBaseResult delete(String namespace, String name); + + /** + * 判断资源是否达到限制 + * + * @param cpuNum 单位为m 1核等于1000m + * @param memNum 单位为Mi 1Mi等于1024Ki + * @param gpuNum 单位为显卡,即"1"表示1张显卡 + * @return LimitsOfResourcesEnum 资源超限枚举类 + */ + LimitsOfResourcesEnum reachLimitsOfResources(String namespace,Integer cpuNum, Integer memNum, Integer gpuNum); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/TrainJobApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/TrainJobApi.java new file mode 100644 index 0000000..c0e81d1 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/TrainJobApi.java @@ -0,0 +1,64 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.bo.PtJupyterJobBO; +import org.dubhe.k8s.domain.resource.BizJob; +import org.dubhe.k8s.domain.vo.PtJupyterJobVO; + +import java.util.List; + +/** + * @description 训练任务操作接口 + * @date 2020-07-03 + */ +public interface TrainJobApi { + /** + * 创建训练任务 Job + * + * @param bo 训练任务 Job BO + * @return PtJupyterJobVO 训练任务 Job 结果类 + */ + PtJupyterJobVO create(PtJupyterJobBO bo); + + /** + * 根据命名空间和资源名删除Job + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return Boolean true删除成功 false删除失败 + */ + Boolean delete(String namespace, String resourceName); + + /** + * 根据命名空间查询Job + * + * @param namespace 命名空间 + * @return List Job业务类集合 + */ + List list(String namespace); + + /** + * 根据命名空间和资源名查询Job + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizJob Job业务类 + */ + BizJob get(String namespace, String resourceName); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/VolumeApi.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/VolumeApi.java new file mode 100644 index 0000000..9df86c4 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/VolumeApi.java @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api; + +import org.dubhe.k8s.domain.bo.BuildFsVolumeBO; +import org.dubhe.k8s.domain.vo.VolumeVO; + +/** + * @description Kubernetes Volume api + * @date 2020-09-10 + */ +public interface VolumeApi { + VolumeVO buildFsVolumes(BuildFsVolumeBO bo); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DistributeTrainApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DistributeTrainApiImpl.java new file mode 100644 index 0000000..1b5c9e5 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DistributeTrainApiImpl.java @@ -0,0 +1,516 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.ResourceRequirements; +import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; +import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinitionList; +import io.fabric8.kubernetes.client.CustomResourceList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.DistributeTrainApi; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.api.ResourceIisolationApi; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.api.VolumeApi; +import org.dubhe.k8s.cache.ResourceCache; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.BuildFsVolumeBO; +import org.dubhe.k8s.domain.bo.DistributeTrainBO; +import org.dubhe.k8s.domain.bo.TaskYamlBO; +import org.dubhe.k8s.domain.cr.DistributeTrain; +import org.dubhe.k8s.domain.cr.DistributeTrainDoneable; +import org.dubhe.k8s.domain.cr.DistributeTrainList; +import org.dubhe.k8s.domain.cr.DistributeTrainSpec; +import org.dubhe.k8s.domain.entity.K8sTask; +import org.dubhe.k8s.domain.resource.BizDistributeTrain; +import org.dubhe.k8s.domain.vo.VolumeVO; +import org.dubhe.k8s.enums.ImagePullPolicyEnum; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.LackOfResourcesEnum; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; +import org.dubhe.k8s.service.K8sTaskService; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.io.ByteArrayInputStream; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.dubhe.biz.base.constant.MagicNumConstant.ONE; +import static org.dubhe.biz.base.constant.MagicNumConstant.SIXTY_LONG; +import static org.dubhe.biz.base.constant.MagicNumConstant.THOUSAND_LONG; +import static org.dubhe.biz.base.constant.MagicNumConstant.ZERO; +import static org.dubhe.biz.base.constant.SymbolConstant.BLANK; +import static org.dubhe.k8s.constant.K8sParamConstants.GPU_RESOURCE_KEY; +import static org.dubhe.k8s.constant.K8sParamConstants.NODE_READY_TRUE; +import static org.dubhe.k8s.constant.K8sParamConstants.PYTHONUNBUFFERED; +import static org.dubhe.k8s.constant.K8sParamConstants.QUANTITY_CPU_KEY; +import static org.dubhe.k8s.constant.K8sParamConstants.QUANTITY_MEMORY_KEY; + +/** + * @description DistributeTrain 实现类 + * @date 2020-07-07 + */ +public class DistributeTrainApiImpl implements DistributeTrainApi { + private static final String CRD_NAME = "distributetrains.onebrain.oneflow.org"; + private static final String CRD_GROUP = "onebrain.oneflow.org"; + private static final String VERSION = "v1alpha1"; + private static final String PLURAL = "distributetrains"; + private static final String SCOPE = "Namespaced"; + private static final String ENABLE_USER_OP = "ENABLE_USER_OP"; + private static final String DATA_ROOT = "DATA_ROOT"; + private static final String NODE_NUM = "NODE_NUM"; + private static final String GPU_NUM_PER_NODE = "GPU_NUM_PER_NODE"; + private static final String ONEFLOW_DEBUG_MODE = "ONEFLOW_DEBUG_MODE"; + private static final String NCCL_DEBUG = "NCCL_DEBUG"; + private static final String INFO = "INFO"; + private static final String DATA_ROOT_VALUE = "/dataset"; + + @javax.annotation.Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + @Autowired + private K8sTaskService k8sTaskService; + + @Autowired + private ResourceCache resourceCache; + + @Autowired + private VolumeApi volumeApi; + + @Autowired + private NodeApi nodeApi; + + @Autowired + private ResourceQuotaApi resourceQuotaApi; + + @Autowired + private ResourceIisolationApi resourceIisolationApi; + + private KubernetesClient client; + private MixedOperation> dtClient; + + public DistributeTrainApiImpl(K8sUtils k8sUtils) { + this.client = k8sUtils.getClient(); + initDtClient(); + } + + /** + * 创建cr + * + * @param bo 自定义资源入参 + * @return BizDistributeTrain 自定义资源dt + */ + @Override + public BizDistributeTrain create(DistributeTrainBO bo) { + LogUtil.info(LogEnum.BIZ_K8S, "Params of creating DistributeTrain--create:{}", bo); + LimitsOfResourcesEnum limitsOfResources = resourceQuotaApi.reachLimitsOfResources(bo.getNamespace(), bo.getCpuNum() * bo.getSize(), bo.getMemNum() * bo.getSize(), bo.getGpuNum() * bo.getSize()); + if (!LimitsOfResourcesEnum.ADEQUATE.equals(limitsOfResources)) { + return new BizDistributeTrain().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), limitsOfResources.getMessage()); + } + if (bo.getGpuNum() != null && bo.getGpuNum() > 0) { + LackOfResourcesEnum lack = nodeApi.isOutOfTotalAllocatableGpu(bo.getGpuNum() * bo.getSize()); + if (!LackOfResourcesEnum.ADEQUATE.equals(lack)) { + return new BizDistributeTrain().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), lack.getMessage()); + } + } + if (!fileStoreApi.createDirs(bo.getDirList().toArray(new String[MagicNumConstant.ZERO]))) { + return new BizDistributeTrain().error(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + VolumeVO volumeVO = volumeApi.buildFsVolumes(new BuildFsVolumeBO(bo.getNamespace(), bo.getName(), bo.getFsMounts())); + if (!K8sResponseEnum.SUCCESS.getCode().equals(volumeVO.getCode())) { + return new BizDistributeTrain().error(volumeVO.getCode(), volumeVO.getMessage()); + } + //删除pod名称缓存 + resourceCache.deletePodCacheByResourceName(bo.getNamespace(), bo.getName()); + return new DistributeTrainDeployer(bo, volumeVO).deploy(); + } + + public class DistributeTrainDeployer { + private String baseName; + private String distributeTrainName; + private String namespace; + private int size; + private String image; + private String masterCmd; + private Integer memNum; + private Integer cpuNum; + private Integer gpuNum; + private String slaveCmd; + private Map env; + private Map baseLabels; + private String businessLabel; + private Integer delayCreate; + private Integer delayDelete; + private TaskYamlBO taskYamlBO; + private VolumeVO volumeVO; + + public DistributeTrainDeployer(DistributeTrainBO bo, VolumeVO volumeVO) { + this.baseName = bo.getName(); + this.distributeTrainName = StrUtil.format(K8sParamConstants.RESOURCE_NAME_TEMPLATE, baseName, RandomUtil.randomString(K8sParamConstants.RESOURCE_NAME_SUFFIX_LENGTH)); + this.namespace = bo.getNamespace(); + this.size = bo.getSize(); + this.image = bo.getImage(); + this.masterCmd = bo.getMasterCmd(); + this.memNum = bo.getMemNum(); + this.cpuNum = bo.getCpuNum(); + this.gpuNum = bo.getGpuNum(); + this.slaveCmd = bo.getSlaveCmd(); + this.env = bo.getEnv(); + this.businessLabel = bo.getBusinessLabel(); + this.baseLabels = LabelUtils.getChildLabels(baseName, distributeTrainName, K8sKindEnum.DISTRIBUTETRAIN.getKind(), businessLabel); + this.delayCreate = bo.getDelayCreateTime(); + this.delayDelete = bo.getDelayDeleteTime(); + this.taskYamlBO = new TaskYamlBO(); + this.volumeVO = volumeVO; + } + + /** + * 判断分布式训练资源定义在集群是否存在 + */ + private boolean isCrdExist() { + CustomResourceDefinitionList crds = client.customResourceDefinitions().list(); + //获取所有自定义资源的定义 + List crdsItems = crds.getItems(); + LogUtil.info(LogEnum.BIZ_K8S, "Found {} CRD(s)", crdsItems.size()); + CustomResourceDefinition crd = null; + //循环判断是否存在分布式训练资源定义 + for (CustomResourceDefinition obj : crdsItems) { + ObjectMeta metadata = obj.getMetadata(); + + if (metadata != null) { + String name = metadata.getName(); + if (CRD_NAME.equals(name)) { + crd = obj; + break; + } + } + } + + if (crd != null) { + LogUtil.info(LogEnum.BIZ_K8S, "Found CRD: {}", crd.getMetadata().getSelfLink()); + return true; + } else { + LogUtil.info(LogEnum.BIZ_K8S, "Not found crd {}", CRD_NAME); + return false; + } + } + + /** + * 构建Spec + */ + private DistributeTrainSpec buildSpec() { + DistributeTrainSpec distributeTrainSpec = new DistributeTrainSpec(); + //配置节点数 + distributeTrainSpec.setSize(size); + //配置镜像和拉取策略 + distributeTrainSpec.setImage(image); + distributeTrainSpec.setImagePullPolicy(ImagePullPolicyEnum.IFNOTPRESENT.getPolicy()); + //配置主从节点运行命令 + distributeTrainSpec.setMasterCmd(masterCmd); + distributeTrainSpec.setSlaveCmd(slaveCmd); + + //master节点申请资源 + ResourceRequirements masterResources = new ResourceRequirements(); + masterResources.setLimits(new HashMap() {{ + Optional.ofNullable(memNum).ifPresent(v -> put(QUANTITY_MEMORY_KEY, new Quantity(v.toString(), K8sParamConstants.MEM_UNIT))); + Optional.ofNullable(cpuNum).ifPresent(v -> put(QUANTITY_CPU_KEY, new Quantity(v.toString(), K8sParamConstants.CPU_UNIT))); + Optional.ofNullable(gpuNum).ifPresent(v -> put(GPU_RESOURCE_KEY, new Quantity(v.toString()))); + }}); + distributeTrainSpec.setMasterResources(masterResources); + //slave节点申请资源 + ResourceRequirements slaveResources = new ResourceRequirements(); + slaveResources.setLimits(new HashMap() {{ + Optional.ofNullable(memNum).ifPresent(v -> put(QUANTITY_MEMORY_KEY, new Quantity(v.toString(), K8sParamConstants.MEM_UNIT))); + Optional.ofNullable(cpuNum).ifPresent(v -> put(QUANTITY_CPU_KEY, new Quantity(v.toString(), K8sParamConstants.CPU_UNIT))); + Optional.ofNullable(gpuNum).ifPresent(v -> put(GPU_RESOURCE_KEY, new Quantity(v.toString()))); + }}); + distributeTrainSpec.setSlaveResources(slaveResources); + //配置环境变量 + List envVarList = new ArrayList() {{ + add(new EnvVarBuilder().withName(PYTHONUNBUFFERED).withValue(SymbolConstant.ZERO).build()); + add(new EnvVarBuilder().withName(ENABLE_USER_OP).withValue(NODE_READY_TRUE).build()); + add(new EnvVarBuilder().withName(DATA_ROOT).withValue(DATA_ROOT_VALUE).build()); + add(new EnvVarBuilder().withName(NODE_NUM).withValue(String.valueOf(size)).build()); + add(new EnvVarBuilder().withName(ONEFLOW_DEBUG_MODE).withValue(BLANK).build()); + add(new EnvVarBuilder().withName(NCCL_DEBUG).withValue(INFO).build()); + }}; + + if (gpuNum != null && gpuNum != 0) { + envVarList.add(new EnvVarBuilder().withName(GPU_NUM_PER_NODE).withValue(String.valueOf(gpuNum)).build()); + } + if (CollectionUtils.isNotEmpty(env)) { + Set envNames = env.keySet(); + for (String envName : envNames) { + envVarList.add(new EnvVarBuilder().withName(envName).withValue(env.get(envName)).build()); + } + } + + distributeTrainSpec.setEnv(envVarList); + distributeTrainSpec.setVolumeMounts(volumeVO.getVolumeMounts()); + distributeTrainSpec.setVolumes(volumeVO.getVolumes()); + + return distributeTrainSpec; + } + + /** + * 判断分布式训练资源在集群是否存在 + */ + private BizDistributeTrain alreadyHaveDistributeTrain() { + CustomResourceList dummyList = dtClient.inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + List dummyItems = dummyList.getItems(); + if (CollectionUtil.isNotEmpty(dummyItems)) { + DistributeTrain distributeTrain = dummyItems.get(0); + LogUtil.warn(LogEnum.BIZ_K8S, "Skip creating DistributeTrain, {} already exists", baseName); + return BizConvertUtils.toBizDistributeTrain(distributeTrain); + } + return null; + } + + /** + * 部署分布式训练资源 + */ + private BizDistributeTrain deploy() { + try { + if (!isCrdExist()) { + return null; + } + + BizDistributeTrain bizdistributeTrain = alreadyHaveDistributeTrain(); + if (bizdistributeTrain != null) { + return bizdistributeTrain; + } + + DistributeTrain distributeTrain = new DistributeTrain(); + ObjectMeta metadata = new ObjectMeta(); + metadata.setName(distributeTrainName); + metadata.setNamespace(namespace); + metadata.setLabels(baseLabels); + distributeTrain.setMetadata(metadata); + + distributeTrain.setSpec(buildSpec()); + + delayCreate = delayCreate == null || delayCreate <= 0 ? ZERO : delayCreate; + delayDelete = delayDelete == null || delayDelete <= 0 ? ZERO : delayDelete; + + if (delayCreate > ZERO || delayDelete > ZERO) { + taskYamlBO.append(distributeTrain); + long applyUnixTime = System.currentTimeMillis() / THOUSAND_LONG + delayCreate * SIXTY_LONG; + Timestamp applyDisplayTime = new Timestamp(applyUnixTime * THOUSAND_LONG); + long stopUnixTime = applyUnixTime + delayDelete * SIXTY_LONG; + Timestamp stopDisplayTime = new Timestamp(stopUnixTime * THOUSAND_LONG); + K8sTask k8sTask = new K8sTask() {{ + setNamespace(namespace); + setResourceName(baseName); + setTaskYaml(JSON.toJSONString(taskYamlBO)); + setBusiness(businessLabel); + setApplyUnixTime(applyUnixTime); + setApplyDisplayTime(applyDisplayTime); + setApplyStatus(delayCreate == ZERO ? ZERO : ONE); + }}; + if (delayDelete > ZERO) { + k8sTask.setStopUnixTime(stopUnixTime); + k8sTask.setStopDisplayTime(stopDisplayTime); + k8sTask.setStopStatus(ONE); + } + k8sTaskService.createOrUpdateTask(k8sTask); + } + if (delayCreate == null || delayCreate == 0) { + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}", distributeTrainName); + resourceIisolationApi.addIisolationInfo(distributeTrain); + distributeTrain = dtClient.inNamespace(namespace).create(distributeTrain); + LogUtil.info(LogEnum.BIZ_K8S, "{} deployed successfully", distributeTrainName); + } + + return BizConvertUtils.toBizDistributeTrain(distributeTrain); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "DistributeTrainApi.deploy过程错误, 错误信息为{}", e.toString()); + return new BizDistributeTrain().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + } + + /** + * 删除 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult deleteByResourceName(String namespace, String resourceName) { + LogUtil.info(LogEnum.BIZ_K8S, "deleteByResourceName namespace {} resourceName {}", namespace,resourceName); + if (dtClient == null) { + LogUtil.error(LogEnum.BIZ_K8S, "dtClient初始化失败"); + } + if (StringUtils.isBlank(namespace) || StringUtils.isBlank(resourceName)) { + return new BizDistributeTrain().baseErrorBadRequest(); + } + try { + k8sTaskService.deleteByNamespaceAndResourceName(namespace,resourceName); + //根据条件获得对应的分布式训练资源集合 + DistributeTrainList list = dtClient.inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + List items = list.getItems(); + //判断该分布式训练资源是否存在 + if (items.size() == 0) { + return new PtBaseResult(K8sResponseEnum.SUCCESS.getCode(), "k8s任务不存在,无需删除"); + } + //判断是否存在多个分布式训练资源 + if (items.size() > 1) { + LogUtil.error(LogEnum.BIZ_K8S, "DistributeTrainApiImpl.deleteByResourceName过程错误, 存在多个dt"); + return new PtBaseResult(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + + if (dtClient.delete(items.get(0))) { + LogUtil.info(LogEnum.BIZ_K8S, "input namespace={};resourceName={}; 删除成功"); + return new PtBaseResult(); + } else { + return new PtBaseResult(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "DistributeTrainApiImpl.deleteByResourceName过程错误, 错误信息为{}", e); + return new BizDistributeTrain().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 根据namespace和resourceName查询cr信息 + * + * @param namespce 命令空间 + * @param resourceName 资源名称 + * @return BizDistributeTrain 自定义资源转换类集合 + */ + @Override + public BizDistributeTrain get(String namespce, String resourceName) { + try { + DistributeTrain distributeTrain = dtClient.inNamespace(namespce).withName(resourceName).get(); + if (distributeTrain != null) { + return BizConvertUtils.toBizDistributeTrain(distributeTrain); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "DistributeTrainApiImpl.get查询错误,错误信息为{}", e.toString()); + return new BizDistributeTrain().error(String.valueOf(e.getCode()), e.getMessage()); + } + return null; + } + + /** + * 根据名称查cr + * + * @param crName 自定义资源名称 + * @return BizDistributeTrain 自定义资源类 + */ + @Override + public BizDistributeTrain findDisByName(String crName) { + //获取所有DistributeTrain资源 + List distributeTrains = dtClient.inAnyNamespace().list().getItems(); + if (!CollectionUtil.isEmpty(distributeTrains)) { + for (DistributeTrain distributeTrain : distributeTrains) { + ObjectMeta metadata = distributeTrain.getMetadata(); + if (metadata != null) { + if (crName != null && crName.equals(metadata.getName())) { + return BizConvertUtils.toBizDistributeTrain(distributeTrain); + } + } + } + } + return null; + } + + /** + * 通过 yaml创建 + * @param crYaml cr定义yaml脚本 + * @return BizDistributeTrain 自定义资源类 + */ + @Override + public BizDistributeTrain create(String crYaml) { + try { + DistributeTrain distributeTrain = dtClient.load(new ByteArrayInputStream(crYaml.getBytes())).createOrReplace(); + return BizConvertUtils.toBizDistributeTrain(distributeTrain); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "Create DistributeTrain error:{} ,yml:{}", e.getMessage(), crYaml); + return null; + } + } + + /** + * 通过 yaml删除 + * @param crYaml cr定义yaml脚本 + * @return boolean true 删除成功 false删除失败 + */ + @Override + public boolean delete(String crYaml) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "delete crYaml {}",crYaml); + return dtClient.load(new ByteArrayInputStream(crYaml.getBytes())).delete(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "Delete DistributeTrain error:{} ,yml:{}", e.getMessage(), crYaml); + return false; + } + } + + /** + * 初始化dtClient + */ + void initDtClient() { + LogUtil.info(LogEnum.BIZ_K8S, "dtClient初始化开始"); + try { + //构建CustomResourceDefinitionContext + CustomResourceDefinitionContext crdContext = new CustomResourceDefinitionContext.Builder() + .withGroup(CRD_GROUP) + .withName(CRD_NAME) + .withPlural(PLURAL) + .withScope(SCOPE) + .withVersion(VERSION) + .build(); + + dtClient = client.customResources(crdContext, DistributeTrain.class, DistributeTrainList.class, DistributeTrainDoneable.class); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_K8S, "dtClient初始化失败, 错误信息为{}", e); + } + LogUtil.info(LogEnum.BIZ_K8S, "dtClient初始化完成"); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DubheDeploymentApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DubheDeploymentApiImpl.java new file mode 100644 index 0000000..f688dd1 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/DubheDeploymentApiImpl.java @@ -0,0 +1,439 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.google.common.collect.Maps; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.DubheDeploymentApi; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.api.ResourceIisolationApi; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.cache.ResourceCache; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtModelOptimizationDeploymentBO; +import org.dubhe.k8s.domain.resource.BizDeployment; +import org.dubhe.k8s.enums.ImagePullPolicyEnum; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.LackOfResourcesEnum; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; +import org.dubhe.k8s.enums.RestartPolicyEnum; +import org.dubhe.k8s.enums.ShellCommandEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.dubhe.k8s.utils.YamlUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @description DubheDeploymentApi 实现类 + * @date 2020-05-26 + */ +public class DubheDeploymentApiImpl implements DubheDeploymentApi { + private K8sUtils k8sUtils; + private KubernetesClient client; + + @Autowired + private NodeApi nodeApi; + @Autowired + private ResourceCache resourceCache; + @Autowired + private ResourceQuotaApi resourceQuotaApi; + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + @Autowired + private ResourceIisolationApi resourceIisolationApi; + + private static final String DATASET = "/dataset"; + private static final String WORKSPACE = "/workspace"; + private static final String OUTPUT = "/output"; + + private static final String PVC_DATASET = "pvc-dataset"; + private static final String PVC_WORKSPACE = "pvc-workspace"; + private static final String PVC_OUTPUT = "pvc-output"; + + public DubheDeploymentApiImpl(K8sUtils k8sUtils) { + this.k8sUtils = k8sUtils; + this.client = k8sUtils.getClient(); + } + + /** + * 创建模型压缩Deployment + * + * @param bo 模型压缩 Deployment BO + * @return BizDeployment Deployment 业务类 + */ + @Override + public BizDeployment create(PtModelOptimizationDeploymentBO bo) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Param of create:{}", bo); + LimitsOfResourcesEnum limitsOfResources = resourceQuotaApi.reachLimitsOfResources(bo.getNamespace(), bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LimitsOfResourcesEnum.ADEQUATE.equals(limitsOfResources)) { + return new BizDeployment().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), limitsOfResources.getMessage()); + } + LackOfResourcesEnum lack = nodeApi.isAllocatable(bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LackOfResourcesEnum.ADEQUATE.equals(lack)) { + return new BizDeployment().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), lack.getMessage()); + } + if (!fileStoreApi.createDirs(bo.getWorkspaceDir(), bo.getDatasetDir(), bo.getOutputDir())) { + return new BizDeployment().error(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + resourceCache.deletePodCacheByResourceName(bo.getNamespace(), bo.getName()); + return new DeploymentDeployer(bo).deploy(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "DeploymentApiImpl.create error, param:{} error:{}", bo, e); + return new BizDeployment().error(String.valueOf(e.getCode()), e.getMessage()); + } + + } + + /** + * 通过命名空间和资源名称查找Deployment资源 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizDeployment Deployment 业务类 + */ + @Override + public BizDeployment getWithResourceName(String namespace, String resourceName) { + try { + if (StringUtils.isEmpty(namespace)) { + return new BizDeployment().baseErrorBadRequest(); + } + DeploymentList bizDeploymentList = client.apps().deployments().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if (CollectionUtil.isEmpty(bizDeploymentList.getItems())) { + return new BizDeployment().error(K8sResponseEnum.NOT_FOUND.getCode(), K8sResponseEnum.NOT_FOUND.getMessage()); + } + Deployment deployment = bizDeploymentList.getItems().get(0); + return BizConvertUtils.toBizDeployment(deployment); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "DeploymentApiImpl.getWithResourceName error, param:[namespace]={}, [resourceName]={}, error:{}", namespace, resourceName, e); + return new BizDeployment().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 通过命名空间查找Deployment资源集合 + * + * @param namespace 命名空间 + * @return List Deployment 业务类集合 + */ + @Override + public List getWithNamespace(String namespace) { + List bizDeploymentList = new ArrayList<>(); + DeploymentList deploymentList = client.apps().deployments().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName()).list(); + if (CollectionUtil.isEmpty(deploymentList.getItems())) { + return bizDeploymentList; + } + return BizConvertUtils.toBizDeploymentList(deploymentList.getItems()); + } + + /** + * 查询集群所有Deployment资源 + * + * @return List Deployment 业务类集合 + */ + @Override + public List listAll() { + return client.apps().deployments().inAnyNamespace().withLabels(LabelUtils.withEnvResourceName()).list().getItems().parallelStream().map(obj -> BizConvertUtils.toBizDeployment(obj)).collect(Collectors.toList()); + } + + /** + * 通过资源名进行删除 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult deleteByResourceName(String namespace, String resourceName) { + LogUtil.info(LogEnum.BIZ_K8S, "Param of deleteByResourceName:namespace {} resourceName {}", namespace,resourceName); + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceName)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + client.apps().deployments().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "DeploymentApiImpl.deleteByResourceName error, param:[namespace]={}, [resourceName]={}, error:{}", namespace, resourceName, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + private class DeploymentDeployer { + private String baseName; + private String deploymentName; + + private String namespace; + private String image; + private String datasetDir; + private String datasetMountPath; + private String workspaceDir; + private String workspaceMountPath; + private String outputDir; + private String outputMountPath; + + private List cmdLines; + //数据集默认只读 + private boolean datasetReadOnly; + + private Map resourcesLimitsMap; + private Map baseLabels; + private String businessLabel; + private Integer gpuNum; + + + private DeploymentDeployer(PtModelOptimizationDeploymentBO bo) { + this.baseName = bo.getName(); + this.deploymentName = StrUtil.format(K8sParamConstants.RESOURCE_NAME_TEMPLATE, baseName, RandomUtil.randomString(MagicNumConstant.EIGHT)); + this.namespace = bo.getNamespace(); + this.image = bo.getImage(); + this.datasetDir = bo.getDatasetDir(); + this.datasetMountPath = StringUtils.isEmpty(bo.getDatasetMountPath()) ? DATASET : bo.getDatasetMountPath(); + this.workspaceDir = bo.getWorkspaceDir(); + this.workspaceMountPath = StringUtils.isEmpty(bo.getWorkspaceMountPath()) ? WORKSPACE : bo.getWorkspaceMountPath(); + this.outputDir = bo.getOutputDir(); + this.outputMountPath = StringUtils.isEmpty(bo.getOutputMountPath()) ? OUTPUT : bo.getOutputMountPath(); + this.cmdLines = new ArrayList(); + this.gpuNum = bo.getGpuNum(); + Optional.ofNullable(bo.getDatasetReadOnly()).ifPresent(v -> datasetReadOnly = v); + Optional.ofNullable(bo.getCmdLines()).ifPresent(v -> cmdLines = v); + + this.resourcesLimitsMap = Maps.newHashMap(); + Optional.ofNullable(bo.getCpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_CPU_KEY, new Quantity(v.toString(), K8sParamConstants.CPU_UNIT))); + Optional.ofNullable(bo.getGpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.GPU_RESOURCE_KEY, new Quantity(v.toString()))); + Optional.ofNullable(bo.getMemNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_MEMORY_KEY, new Quantity(v.toString(), K8sParamConstants.MEM_UNIT))); + this.businessLabel = bo.getBusinessLabel(); + this.baseLabels = LabelUtils.getBaseLabels(baseName, businessLabel); + + this.datasetReadOnly = true; + } + + /** + * 部署Deployment + * + * @return BizDeployment Deployment 业务类 + */ + public BizDeployment deploy() { + //部署deployment + try { + Deployment deployment = deployDeployment(); + return BizConvertUtils.toBizDeployment(deployment); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "DeploymentApiImpl.deploy error:{}", e); + return (BizDeployment) new PtBaseResult().error(String.valueOf(e.getCode()), e.getMessage()); + } + + } + + /** + * 检查资源是否已经存在 + * + * @return Deployment Deployment 业务类 + */ + private Deployment alreadyHaveDeployment() { + DeploymentList list = client.apps().deployments().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + if (CollectionUtil.isNotEmpty(list.getItems())) { + Deployment deployment = list.getItems().get(0); + LogUtil.info(LogEnum.BIZ_K8S, "Skip creating job, {} already exists", deployment.getMetadata().getName()); + return deployment; + } + return null; + } + + /** + * 部署Deployment + * + * @return Deployment Deployment 业务类 + */ + private Deployment deployDeployment() { + //已经存在直接返回 + Deployment deployment = alreadyHaveDeployment(); + if (deployment != null) { + return deployment; + } + deployment = buildDeployment(); + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(deployment)); + resourceIisolationApi.addIisolationInfo(deployment); + deployment = client.apps().deployments().inNamespace(namespace).create(deployment); + return deployment; + } + + /** + * 构建Deployment + * + * @return Deployment Deployment 业务类 + */ + private Deployment buildDeployment() { + Map childLabels = LabelUtils.getChildLabels(baseName, deploymentName, K8sKindEnum.DEPLOYMENT.getKind(), businessLabel); + LabelSelector labelSelector = new LabelSelector(); + labelSelector.setMatchLabels(childLabels); + return new DeploymentBuilder() + .withNewMetadata() + .withName(deploymentName) + .addToLabels(baseLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .withSelector(labelSelector) + .withNewTemplate() + .withNewMetadata() + .withName(deploymentName) + .addToLabels(childLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .addToNodeSelector(gpuSelector()) + .addToContainers(buildContainer()) + .addToVolumes(buildVolume().toArray(new Volume[0])) + .withRestartPolicy(RestartPolicyEnum.ALWAYS.getRestartPolicy()) + .endSpec() + .endTemplate() + .endSpec() + .build(); + } + + /** + * 添加Gpu label + * + * @return Map Gpu label 键值 + */ + private Map gpuSelector() { + Map gpuSelector = new HashMap<>(2); + if (gpuNum != null && gpuNum > 0) { + gpuSelector.put(K8sLabelConstants.NODE_GPU_LABEL_KEY, K8sLabelConstants.NODE_GPU_LABEL_VALUE); + } + return gpuSelector; + } + + /** + * 构建Container + * + * @return Container 容器 + */ + private Container buildContainer() { + return new ContainerBuilder() + .withNewName(deploymentName) + .withNewImage(image) + .withNewImagePullPolicy(ImagePullPolicyEnum.IFNOTPRESENT.getPolicy()) + .withVolumeMounts(buildVolumeMount()) + .addNewCommand(ShellCommandEnum.BIN_BANSH.getShell()) + .addAllToArgs(cmdLines) + .withNewResources().addToLimits(resourcesLimitsMap).endResources() + .build(); + } + + /** + * 构建VolumeMount + * + * @return List VolumeMount集合类 + */ + private List buildVolumeMount() { + List volumeMounts = new ArrayList<>(); + if (StrUtil.isNotBlank(datasetDir)) { + volumeMounts.add(new VolumeMountBuilder() + .withName(PVC_DATASET) + .withMountPath(datasetMountPath) + .withReadOnly(datasetReadOnly) + .build()); + } + if (StrUtil.isNotBlank(workspaceDir)) { + volumeMounts.add(new VolumeMountBuilder() + .withName(PVC_WORKSPACE) + .withMountPath(workspaceMountPath) + .withReadOnly(false) + .build()); + } + if (StrUtil.isNotBlank(outputDir)) { + volumeMounts.add(new VolumeMountBuilder() + .withName(PVC_OUTPUT) + .withMountPath(outputMountPath) + .withReadOnly(false) + .build()); + } + return volumeMounts; + } + + /** + * 构建Volume + * + * @return List Volume集合类 + */ + private List buildVolume() { + List volumes = new ArrayList<>(); + if (StrUtil.isNotBlank(datasetDir)) { + volumes.add(new VolumeBuilder() + .withName(PVC_DATASET) + .withNewHostPath() + .withPath(datasetDir) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + } + if (StrUtil.isNotBlank(workspaceDir)) { + volumes.add(new VolumeBuilder() + .withName(PVC_WORKSPACE) + .withNewHostPath() + .withPath(workspaceDir) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + } + if (StrUtil.isNotBlank(outputDir)) { + volumes.add(new VolumeBuilder() + .withName(PVC_OUTPUT) + .withNewHostPath() + .withPath(outputDir) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + } + return volumes; + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/JupyterResourceApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/JupyterResourceApiImpl.java new file mode 100644 index 0000000..e74b306 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/JupyterResourceApiImpl.java @@ -0,0 +1,692 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.SecretList; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServiceList; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; +import io.fabric8.kubernetes.api.model.apps.StatefulSetList; +import io.fabric8.kubernetes.api.model.extensions.Ingress; +import io.fabric8.kubernetes.api.model.extensions.IngressBuilder; +import io.fabric8.kubernetes.api.model.extensions.IngressList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.JupyterResourceApi; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.api.PersistentVolumeClaimApi; +import org.dubhe.k8s.api.ResourceIisolationApi; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.cache.ResourceCache; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtJupyterResourceBO; +import org.dubhe.k8s.domain.bo.PtPersistentVolumeClaimBO; +import org.dubhe.k8s.domain.bo.TaskYamlBO; +import org.dubhe.k8s.domain.entity.K8sTask; +import org.dubhe.k8s.domain.resource.BizPersistentVolumeClaim; +import org.dubhe.k8s.domain.vo.PtJupyterDeployVO; +import org.dubhe.k8s.enums.ImagePullPolicyEnum; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.LackOfResourcesEnum; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; +import org.dubhe.k8s.service.K8sTaskService; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.dubhe.k8s.utils.YamlUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Resource; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; +import static org.dubhe.biz.base.constant.MagicNumConstant.SIXTY_LONG; +import static org.dubhe.biz.base.constant.MagicNumConstant.THOUSAND_LONG; +import static org.dubhe.biz.base.constant.MagicNumConstant.ZERO; +import static org.dubhe.biz.base.constant.MagicNumConstant.ZERO_LONG; + +/** + * @description JupyterResourceApi 实现类 + * @date 2020-04-17 + */ +public class JupyterResourceApiImpl implements JupyterResourceApi { + + private K8sUtils k8sUtils; + private KubernetesClient client; + @Autowired + private PersistentVolumeClaimApi persistentVolumeClaimApi; + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + @Autowired + private NodeApi nodeApi; + @Autowired + private K8sTaskService k8sTaskService; + @Autowired + private ResourceCache resourceCache; + @Autowired + private ResourceQuotaApi resourceQuotaApi; + @Autowired + private ResourceIisolationApi resourceIisolationApi; + + + private static final String DATASET = "/dataset"; + private static final String WORKSPACE = "/workspace"; + + private static final String PVC_DATASET = "pvc-dataset"; + private static final String PVC_WORKSPACE = "pvc-workspace"; + + private static final String CONTAINER_NAME = "web"; + private static final Integer CONTAINER_PORT = 8888; + private static final Integer SVC_PORT = 32680; + private static final String NOTEBOOK_MAX_UPLOAD_SIZE = "100m"; + + public JupyterResourceApiImpl(K8sUtils k8sUtils) { + this.k8sUtils = k8sUtils; + this.client = k8sUtils.getClient(); + } + + /** + * 创建Notebook + * + * @param bo 模型管理 Notebook BO + * @return PtJupyterDeployVO Notebook 结果类 + */ + @Override + public PtJupyterDeployVO create(PtJupyterResourceBO bo) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Param of creating Notebook--create:{}", bo); + if (null == bo) { + return new PtJupyterDeployVO().error(K8sResponseEnum.BAD_REQUEST.getCode(), K8sResponseEnum.BAD_REQUEST.getMessage()); + } + + LimitsOfResourcesEnum limitsOfResources = resourceQuotaApi.reachLimitsOfResources(bo.getNamespace(), bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LimitsOfResourcesEnum.ADEQUATE.equals(limitsOfResources)) { + return new PtJupyterDeployVO().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), limitsOfResources.getMessage()); + } + LackOfResourcesEnum lack = nodeApi.isAllocatable(bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LackOfResourcesEnum.ADEQUATE.equals(lack)) { + return new PtJupyterDeployVO().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), lack.getMessage()); + } + + if (!fileStoreApi.createDirs(bo.getWorkspaceDir(), bo.getDatasetDir())) { + return new PtJupyterDeployVO().error(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + resourceCache.deletePodCacheByResourceName(bo.getNamespace(), bo.getName()); + PtJupyterDeployVO result = new JupyterDeployer(bo).buildFsVolumes().deploy(); + LogUtil.info(LogEnum.BIZ_K8S, "Return value of creating Notebook create:{}", result); + return result; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "JupyterResourceApiImpl.create error, param:{} error:{}", bo, e); + return new PtJupyterDeployVO().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 挂载存储创建 Notebook + * + * @param bo 模型管理 Notebook BO + * @return PtJupyterDeployVO Notebook 结果类 + */ + @Override + public PtJupyterDeployVO createWithPvc(PtJupyterResourceBO bo) { + try { + LimitsOfResourcesEnum limitsOfResources = resourceQuotaApi.reachLimitsOfResources(bo.getNamespace(), bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LimitsOfResourcesEnum.ADEQUATE.equals(limitsOfResources)) { + return new PtJupyterDeployVO().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), limitsOfResources.getMessage()); + } + LackOfResourcesEnum lack = nodeApi.isAllocatable(bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LackOfResourcesEnum.ADEQUATE.equals(lack)) { + return new PtJupyterDeployVO().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), lack.getMessage()); + } + LogUtil.info(LogEnum.BIZ_K8S, "Param of creating Notebook--createWithPvc:{}", bo); + if (null == bo) { + return new PtJupyterDeployVO().error(K8sResponseEnum.BAD_REQUEST.getCode(), K8sResponseEnum.BAD_REQUEST.getMessage()); + } + if (!fileStoreApi.createDirs(bo.getWorkspaceDir(), bo.getDatasetDir())) { + return new PtJupyterDeployVO().error(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + BizPersistentVolumeClaim bizPersistentVolumeClaim = (StringUtils.isEmpty(bo.getWorkspaceDir())) ? persistentVolumeClaimApi.createDynamicNfs(new PtPersistentVolumeClaimBO(bo)) : persistentVolumeClaimApi.createWithFsPv(new PtPersistentVolumeClaimBO(bo)); + if (K8sResponseEnum.SUCCESS.getCode().equals(bizPersistentVolumeClaim.getCode())) { + bo.setWorkspacePvcName(bizPersistentVolumeClaim.getName()); + resourceCache.deletePodCacheByResourceName(bo.getNamespace(), bo.getName()); + PtJupyterDeployVO result = new JupyterDeployer(bo).buildFsPvcVolumes().deploy(); + LogUtil.info(LogEnum.BIZ_K8S, "Return value of creating Notebook--createWithPvc:{}", result); + return result; + } else { + LogUtil.info(LogEnum.BIZ_K8S, "Notebook--createWithPvc error:{}", bizPersistentVolumeClaim.getMessage()); + return new PtJupyterDeployVO().error(bizPersistentVolumeClaim.getCode(), bizPersistentVolumeClaim.getMessage()); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "JupyterResourceApiImpl.createWithPvc error, param:{} error:{}", bo, e); + return new PtJupyterDeployVO().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除 Notebook + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult delete(String namespace, String resourceName) { + LogUtil.info(LogEnum.BIZ_K8S, "Param of delete namespace {} resourceName {}", namespace,resourceName); + try { + Boolean res = client.extensions().ingresses().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete() + && client.services().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete() + && client.apps().statefulSets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete() + && client.secrets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete(); + k8sTaskService.deleteByNamespaceAndResourceName(namespace,resourceName); + if (res) { + return new PtBaseResult(); + } else { + return K8sResponseEnum.REPEAT.toPtBaseResult(); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "JupyterResourceApiImpl.delete error:{}", e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 查询命名空间下所有Notebook + * + * @param namespace 命名空间 + * @return List Notebook 结果类集合 + */ + @Override + public List list(String namespace) { + StatefulSetList list = client.apps().statefulSets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName()).list(); + if (CollectionUtil.isEmpty(list.getItems())) { + return Collections.EMPTY_LIST; + } + return list.getItems().stream().map(ss -> { + Map labels = ss.getMetadata().getLabels(); + String resourceName = labels.get(K8sLabelConstants.BASE_TAG_SOURCE); + return get(namespace, resourceName); + }).collect(toList()); + } + + /** + * 查询Notebook + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtJupyterDeployVO Notebook 结果类 + */ + @Override + public PtJupyterDeployVO get(String namespace, String resourceName) { + try { + IngressList ingressList = client.extensions().ingresses().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + Ingress ingress = CollectionUtil.isEmpty(ingressList.getItems()) ? null : ingressList.getItems().get(0); + ServiceList svcList = client.services().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + Service svc = CollectionUtil.isEmpty(svcList.getItems()) ? null : svcList.getItems().get(0); + StatefulSetList ssList = client.apps().statefulSets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + StatefulSet statefulSet = CollectionUtil.isEmpty(ssList.getItems()) ? null : ssList.getItems().get(0); + SecretList secretList = client.secrets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + Secret secret = CollectionUtil.isEmpty(secretList.getItems()) ? null : secretList.getItems().get(0); + return new PtJupyterDeployVO(secret, statefulSet, svc, ingress); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "JupyterResourceApiImpl.get error:{}", e); + return new PtJupyterDeployVO().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + private class JupyterDeployer { + private static final String SUB_RESOURCE_NAME_TEMPLATE = "{}-{}-{}"; + + private String baseName; + private String statefulSetName; + private String secretName; + private String svcName; + private String ingressName; + + private String namespace; + private String image; + private String datasetDir; + private String datasetMountPath; + private String workspaceMountPath; + private String workspaceDir; + private Boolean useGpu; + + //数据集默认只读 + private boolean datasetReadOnly; + private String workSpacePvcName; + private String host; + + private Map resourcesLimitsMap; + private Map baseLabels; + private Map podLabels; + + private String defaultJupyterPwd; + private String baseUrl; + private String secondaryDomain; + private String businessLabel; + private Integer delayDelete; + + private List volumeMounts; + private List volumes; + private TaskYamlBO taskYamlBO; + + private JupyterDeployer(PtJupyterResourceBO bo) { + this.baseName = bo.getName(); + this.statefulSetName = StrUtil.format(K8sParamConstants.RESOURCE_NAME_TEMPLATE, baseName, RandomUtil.randomString(MagicNumConstant.FIVE)); + this.namespace = bo.getNamespace(); + this.image = bo.getImage(); + this.datasetDir = bo.getDatasetDir(); + this.datasetMountPath = StringUtils.isEmpty(bo.getDatasetMountPath()) ? DATASET : bo.getDatasetMountPath(); + this.workspaceDir = bo.getWorkspaceDir(); + this.workspaceMountPath = StringUtils.isEmpty(bo.getWorkspaceMountPath()) ? WORKSPACE : bo.getWorkspaceMountPath(); + Optional.ofNullable(bo.getDatasetReadOnly()).ifPresent(v -> datasetReadOnly = v); + this.workSpacePvcName = bo.getWorkspacePvcName(); + this.useGpu = bo.getUseGpu() == null ? false : bo.getUseGpu(); + if (bo.getUseGpu() != null && bo.getUseGpu() && null == bo.getGpuNum()) { + bo.setGpuNum(0); + } + + this.resourcesLimitsMap = Maps.newHashMap(); + Optional.ofNullable(bo.getCpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_CPU_KEY, new Quantity(v.toString(), K8sParamConstants.CPU_UNIT))); + Optional.ofNullable(bo.getGpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.GPU_RESOURCE_KEY, new Quantity(v.toString()))); + Optional.ofNullable(bo.getMemNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_MEMORY_KEY, new Quantity(v.toString(), K8sParamConstants.MEM_UNIT))); + + this.host = k8sUtils.getHost(); + this.businessLabel = bo.getBusinessLabel(); + this.delayDelete = bo.getDelayDeleteTime(); + this.baseLabels = LabelUtils.getBaseLabels(baseName, businessLabel); + this.podLabels = LabelUtils.getChildLabels(baseName, statefulSetName, K8sKindEnum.STATEFULSET.getKind(), businessLabel); + //生成附属资源的名称 + generateResourceName(); + + this.defaultJupyterPwd = RandomUtil.randomNumbers(MagicNumConstant.SIX); + this.baseUrl = SymbolConstant.SLASH + RandomUtil.randomString(MagicNumConstant.SIX); + this.secondaryDomain = RandomUtil.randomString(MagicNumConstant.SIX) + SymbolConstant.DOT; + + this.datasetReadOnly = true; + this.volumeMounts = new ArrayList(); + this.volumes = new ArrayList(); + this.taskYamlBO = new TaskYamlBO(); + } + + /** + * 部署Notebook + * + * @return PtJupyterDeployVO Notebook 结果类 + */ + public PtJupyterDeployVO deploy() { + //部署secret + Secret secret = deploySecret(Base64.encode(defaultJupyterPwd), Base64.encode(baseUrl)); + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(secret)); + //部署statefulset + StatefulSet statefulSet = deployStatefulSet(); + //部署svc + Service service = deployService(); + //部署ingress + Ingress ingress = deployIngress(); + + if (delayDelete != null && delayDelete > ZERO) { + taskYamlBO.append(secret); + taskYamlBO.append(statefulSet); + taskYamlBO.append(service); + taskYamlBO.append(ingress); + + long stopUnixTime = System.currentTimeMillis() / THOUSAND_LONG + delayDelete * SIXTY_LONG; + Timestamp stopDisplayTime = new Timestamp(stopUnixTime * THOUSAND_LONG); + K8sTask k8sTask = new K8sTask() {{ + setNamespace(namespace); + setResourceName(baseName); + setTaskYaml(JSON.toJSONString(taskYamlBO)); + setBusiness(businessLabel); + setStopUnixTime(stopUnixTime); + setStopDisplayTime(stopDisplayTime); + setStopStatus(MagicNumConstant.ONE); + }}; + k8sTaskService.createOrUpdateTask(k8sTask); + } + + return new PtJupyterDeployVO(secret, statefulSet, service, ingress); + } + + /** + * 部署secret + * + * @param base64Pwd base64密码 + * @param base64BaseUrl base64路径 + * @return Secret 加密后的密码 + */ + private Secret deploySecret(String base64Pwd, String base64BaseUrl) { + Secret secret = null; + SecretList list = client.secrets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + if (CollectionUtil.isNotEmpty(list.getItems())) { + secret = list.getItems().get(0); + secretName = secret.getMetadata().getName(); + LogUtil.info(LogEnum.BIZ_K8S, "Skip creating secret, {} already exists", secretName); + } else { + secret = new SecretBuilder() + .withNewMetadata() + .withName(secretName) + .addToLabels(baseLabels) + .withNamespace(namespace) + .endMetadata() + .addToData(ImmutableMap.of(K8sParamConstants.SECRET_PWD_KEY, base64Pwd, K8sParamConstants.SECRET_URL_KEY, base64BaseUrl)) + .build(); + + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}", secretName); + secret = client.secrets().create(secret); + LogUtil.info(LogEnum.BIZ_K8S, "{} deployed successfully", secretName); + } + + return secret; + } + + /** + * 构建VolumeMount + */ + private void buildDatasetFsVolume() { + //挂载点 + if (StrUtil.isNotBlank(datasetDir)) { + volumeMounts.add(new VolumeMountBuilder() + .withName(PVC_DATASET) + .withMountPath(datasetMountPath) + .withReadOnly(datasetReadOnly) + .build()); + + volumes.add(new VolumeBuilder() + .withName(PVC_DATASET) + .withNewHostPath() + .withPath(datasetDir) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + } + } + + /** + * 构建VolumeMount + */ + private void buildWorkspaceFsVolume() { + if (StrUtil.isNotBlank(workspaceDir)) { + volumeMounts.add(new VolumeMountBuilder() + .withName(PVC_WORKSPACE) + .withMountPath(workspaceMountPath) + .build()); + + volumes.add(new VolumeBuilder() + .withName(PVC_WORKSPACE) + .withNewHostPath() + .withPath(workspaceDir) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + } + } + + /** + * 构建VolumeMount + */ + private void buildWorkspaceFsPvcVolume() { + //挂载点 + if (StrUtil.isNotBlank(workSpacePvcName)) { + volumeMounts.add(new VolumeMountBuilder() + .withName(PVC_WORKSPACE) + .withMountPath(workspaceMountPath) + .build()); + + volumes.add(new VolumeBuilder() + .withName(PVC_WORKSPACE) + .withNewPersistentVolumeClaim(workSpacePvcName, false) + .build()); + } + } + + /** + * 挂载存储 + * + * @return JupyterDeployer Notebook 部署类 + */ + private JupyterDeployer buildFsVolumes() { + buildDatasetFsVolume(); + buildWorkspaceFsVolume(); + return this; + } + + /** + * 按照存储资源声明挂载存储 + * + * @return JupyterDeployer Notebook 部署类 + */ + private JupyterDeployer buildFsPvcVolumes() { + buildDatasetFsVolume(); + buildWorkspaceFsPvcVolume(); + return this; + } + + /** + * 部署statefulset + * + * @return StatefulSet 类 + */ + private StatefulSet deployStatefulSet() { + StatefulSet statefulSet = null; + StatefulSetList list = client.apps().statefulSets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + if (CollectionUtil.isNotEmpty(list.getItems())) { + statefulSet = list.getItems().get(0); + statefulSetName = statefulSet.getMetadata().getName(); + LogUtil.info(LogEnum.BIZ_K8S, "Skip creating statefulSet, {} already exists", statefulSetName); + return statefulSet; + } + LabelSelector labelSelector = new LabelSelector(); + labelSelector.setMatchLabels(ImmutableMap.of(K8sLabelConstants.BASE_TAG_P_NAME, statefulSetName)); + //容器 + Container container = new Container(); + container.setName(statefulSetName); + container.setImage(image); + container.setImagePullPolicy(ImagePullPolicyEnum.IFNOTPRESENT.getPolicy()); + //端口映射 + container.setPorts(Arrays.asList(new ContainerPortBuilder() + .withContainerPort(CONTAINER_PORT) + .withName(CONTAINER_NAME).build())); + container.setVolumeMounts(volumeMounts); + + //环境变量 + List env = new ArrayList(); + env.add(new EnvVarBuilder().withName(K8sParamConstants.ENV_PWD_KEY) + .withNewValueFrom() + .withNewSecretKeyRef() + .withName(secretName) + .withKey(K8sParamConstants.SECRET_PWD_KEY) + .endSecretKeyRef() + .endValueFrom().build()); + env.add(new EnvVarBuilder().withName(K8sParamConstants.ENV_URL_KEY) + .withNewValueFrom() + .withNewSecretKeyRef() + .withName(secretName) + .withKey(K8sParamConstants.SECRET_URL_KEY) + .endSecretKeyRef() + .endValueFrom().build()); + + container.setResources(new ResourceRequirementsBuilder() + .addToLimits(resourcesLimitsMap) + .build()); + Map gpuLabel = new HashMap<>(2); + if (useGpu) { + gpuLabel.put(K8sLabelConstants.NODE_GPU_LABEL_KEY, K8sLabelConstants.NODE_GPU_LABEL_VALUE); + } + + statefulSet = new StatefulSetBuilder() + .withNewMetadata() + .withName(statefulSetName) + .addToLabels(baseLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .withSelector(labelSelector) + .withServiceName(statefulSetName) + .withReplicas(1) + .withNewTemplate() + .withNewMetadata() + .withName(statefulSetName) + .addToLabels(podLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .withTerminationGracePeriodSeconds(ZERO_LONG) + .addToNodeSelector(gpuLabel) + .withTerminationGracePeriodSeconds(SIXTY_LONG) + .addToContainers(container) + .addToVolumes(volumes.toArray(new Volume[0])) + .endSpec() + .endTemplate() + .endSpec() + .build(); + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}, yaml info is : {}", statefulSetName, YamlUtils.dumpAsYaml(statefulSet)); + resourceIisolationApi.addIisolationInfo(statefulSet); + statefulSet = client.apps().statefulSets().create(statefulSet); + LogUtil.info(LogEnum.BIZ_K8S, "{} deployed successfully", statefulSetName); + return statefulSet; + } + + /** + * 部署service + * + * @return Service 类 + */ + private Service deployService() { + Service svc = null; + ServiceList list = client.services().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + if (CollectionUtil.isNotEmpty(list.getItems())) { + svc = list.getItems().get(0); + svcName = svc.getMetadata().getName(); + LogUtil.info(LogEnum.BIZ_K8S, "Skip creating service, {} already exists", svcName); + return svc; + } + svc = new ServiceBuilder() + .withNewMetadata() + .withName(svcName) + .addToLabels(baseLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .addNewPort() + .withPort(SVC_PORT) + .withTargetPort(new IntOrString(CONTAINER_PORT)) + .withName(CONTAINER_NAME) + .endPort() + .withClusterIP("None") + .withSelector(podLabels) + .endSpec() + .build(); + + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}, yaml info is : {}", svcName, YamlUtils.dumpAsYaml(svc)); + svc = client.services().create(svc); + LogUtil.info(LogEnum.BIZ_K8S, "{} deployed successfully", svcName); + return svc; + } + + /** + * 部署ingress + * + * @return Ingress 类 + */ + private Ingress deployIngress() { + Ingress ingress = null; + IngressList list = client.extensions().ingresses().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + if (CollectionUtil.isNotEmpty(list.getItems())) { + ingress = list.getItems().get(0); + ingressName = ingress.getMetadata().getName(); + LogUtil.info(LogEnum.BIZ_K8S, "Skip creating ingress, {} already exists", ingressName); + return ingress; + } + ingress = new IngressBuilder() + .withNewMetadata() + .withName(ingressName) + .addToLabels(baseLabels) + .withNamespace(namespace) + .addToAnnotations(K8sParamConstants.INGRESS_PROXY_BODY_SIZE_KEY, NOTEBOOK_MAX_UPLOAD_SIZE) + .endMetadata() + .withNewSpec() + .addNewRule() + .withHost(secondaryDomain + host) + .withNewHttp() + .withPaths() + .addNewPath() + .withNewPath(SymbolConstant.SLASH) + .withNewBackend() + .withServiceName(svcName) + .withServicePort(new IntOrString(SVC_PORT)) + .endBackend() + .endPath() + .endHttp() + .endRule() + .endSpec() + .build(); + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}, yaml info is : {}", ingressName, YamlUtils.dumpAsYaml(ingress)); + ingress = client.extensions().ingresses().create(ingress); + LogUtil.info(LogEnum.BIZ_K8S, "{} deployed successfully", ingressName); + return ingress; + } + + /** + * 生成资源名 + */ + private void generateResourceName() { + String randomStr = RandomUtil.randomString(MagicNumConstant.FIVE); + this.secretName = StrUtil.format(SUB_RESOURCE_NAME_TEMPLATE, baseName, SymbolConstant.TOKEN, randomStr); + this.ingressName = StrUtil.format(SUB_RESOURCE_NAME_TEMPLATE, baseName, K8sParamConstants.INGRESS_SUFFIX, randomStr); + this.svcName = StrUtil.format(SUB_RESOURCE_NAME_TEMPLATE, baseName, K8sParamConstants.SVC_SUFFIX, randomStr); + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LimitRangeApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LimitRangeApiImpl.java new file mode 100644 index 0000000..ee488f7 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LimitRangeApiImpl.java @@ -0,0 +1,128 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.fabric8.kubernetes.api.model.LimitRange; +import io.fabric8.kubernetes.api.model.LimitRangeBuilder; +import io.fabric8.kubernetes.api.model.LimitRangeItem; +import io.fabric8.kubernetes.api.model.LimitRangeList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.k8s.api.LimitRangeApi; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtLimitRangeBO; +import org.dubhe.k8s.domain.resource.BizLimitRange; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.base.utils.StringUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @description LimitRangeApi 实现类 + * @date 2020-04-23 + */ +public class LimitRangeApiImpl implements LimitRangeApi { + private KubernetesClient client; + + public LimitRangeApiImpl(K8sUtils k8sUtils) { + this.client = k8sUtils.getClient(); + } + + /** + * 创建LimitRange + * + * @param bo LimitRange BO + * @return BizLimitRange LimitRange 业务类 + */ + @Override + public BizLimitRange create(PtLimitRangeBO bo) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Input {}", bo); + Gson gson = new Gson(); + List limits = gson.fromJson(gson.toJson(bo.getLimits()), new TypeToken>() { + }.getType()); + LimitRange limitRange = new LimitRangeBuilder().withNewMetadata().withName(bo.getName()).endMetadata() + .withNewSpec().withLimits(limits).endSpec().build(); + BizLimitRange bizLimitRange = BizConvertUtils.toBizLimitRange(client.limitRanges().inNamespace(bo.getNamespace()).create(limitRange)); + LogUtil.info(LogEnum.BIZ_K8S, "Output {}", bizLimitRange); + return bizLimitRange; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "LimitRangeApiImpl.create error, param:{} error:{}", bo, e); + return new BizLimitRange().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 查询命名空间下所有LimitRange + * + * @param namespace 命名空间 + * @return List LimitRange 业务类集合 + */ + @Override + public List list(String namespace) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Input namespace={}", namespace); + if (StringUtils.isEmpty(namespace)) { + LimitRangeList limitRangeList = client.limitRanges().inAnyNamespace().list(); + return limitRangeList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizLimitRange(obj)).collect(Collectors.toList()); + } else { + LimitRangeList limitRangeList = client.limitRanges().inNamespace(namespace).list(); + List bizLimitRangeList = limitRangeList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizLimitRange(obj)).collect(Collectors.toList()); + LogUtil.info(LogEnum.BIZ_K8S, "Output {}", bizLimitRangeList); + return bizLimitRangeList; + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "LimitRangeApiImpl.list error, param:[namespace]={},error:", namespace, e); + return Collections.EMPTY_LIST; + } + + } + + /** + * 删除LimitRange + * + * @param namespace 命名空间 + * @param name LimitRange 名称 + * @return PtBaseResult 基本结果类 + */ + @Override + public PtBaseResult delete(String namespace, String name) { + LogUtil.info(LogEnum.BIZ_K8S, "Input namespace={};name={}", namespace, name); + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(name)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + if (client.limitRanges().inNamespace(namespace).withName(name).delete()) { + return new PtBaseResult(); + } else { + return K8sResponseEnum.REPEAT.toPtBaseResult(); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "LimitRangeApiImpl.delete error, param:[namespace]={}, [name]={}, error:",namespace, name, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LogMonitoringApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LogMonitoringApiImpl.java new file mode 100644 index 0000000..a61b2d8 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/LogMonitoringApiImpl.java @@ -0,0 +1,369 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import io.fabric8.kubernetes.api.model.DoneablePod; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.PodResource; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.base.utils.TimeTransferUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.LogMonitoringApi; +import org.dubhe.k8s.cache.ResourceCache; +import org.dubhe.k8s.domain.bo.LogMonitoringBO; +import org.dubhe.k8s.domain.vo.LogMonitoringVO; +import org.dubhe.k8s.utils.K8sUtils; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.Operator; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.sort.SortOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +import static org.dubhe.biz.base.constant.MagicNumConstant.ZERO; +import static org.dubhe.biz.base.constant.MagicNumConstant.*; +import static org.dubhe.biz.base.constant.SymbolConstant.*; + + +/** + * @description k8s集群日志查询接口 + * @date 2020-06-29 + */ +public class LogMonitoringApiImpl implements LogMonitoringApi { + @Value("${k8s.elasticsearch.log.source_field}") + private String sourceField; + + @Value("${k8s.elasticsearch.log.type}") + private String type; + + @Autowired + private RestHighLevelClient restHighLevelClient; + + @Autowired + private ResourceCache resourceCache; + + private KubernetesClient kubernetesClient; + private static final String INDEX_NAME = "kubelogs"; + private static final String POD_NAME_KEY = "kubernetes.pod_name.keyword"; + private static final String POD_NAME = "kubernetes.pod_name"; + private static final String NAMESPACE_KEY = "kubernetes.namespace_name.keyword"; + private static final String NAMESPACE = "kubernetes.namespace_name"; + private static final String TIMESTAMP = "@timestamp"; + private static final String MESSAGE = "log"; + private static final String LOG_PREFIX = "[Dubhe Service Log] "; + private static final String INDEX_FORMAT = "yyyy.MM.dd"; + + public LogMonitoringApiImpl(K8sUtils k8sUtils) { + this.kubernetesClient = k8sUtils.getClient(); + } + + + /** + * 添加Pod日志到ES,无日志参数,默认从k8s集群查询日志添加到ES + * + * @param podName Pod名称 + * @param namespace 命名空间 + * @return boolean 日志添加是否成功 + */ + @Override + public boolean addLogsToEs(String podName, String namespace) { + + if (StringUtils.isBlank(podName) || StringUtils.isBlank(namespace)) { + LogUtil.error(LogEnum.BIZ_K8S, "LogMonitoringApiImpl.addLogsToEs error: param [podName] and [namespace] are required"); + return false; + } + List logList = searchLogInfoByEs(ZERO, ONE, new LogMonitoringBO(namespace,podName)); + if (CollectionUtils.isNotEmpty(logList)) { + return true; + } + + String logInfoString = getLogInfoString(podName, namespace); + if (StringUtils.isBlank(logInfoString)) { + LogUtil.info(LogEnum.BIZ_K8S, "LogMonitoringApiImpl.getLogInfoString could not get any log,no doc created in Elasticsearch"); + return false; + } + logList = Arrays.asList(logInfoString.split(LINEBREAK)).stream().limit(TEN_THOUSAND).collect(Collectors.toList()); + return addLogsToEs(podName, namespace, logList); + } + + /** + * 添加Pod自定义日志到ES + * + * @param podName Pod名称 + * @param namespace 命名空间 + * @param logList 日志信息 + * @return boolean 日志添加是否成功 + */ + @Override + public boolean addLogsToEs(String podName, String namespace, List logList) { + Date date = new Date(); + SimpleDateFormat indexFormat = new SimpleDateFormat(INDEX_FORMAT); + String timestamp = TimeTransferUtil.dateTransferToUtc(date); + BulkRequest bulkRequest = new BulkRequest(); + try { + for (int i = 0; i < logList.size(); i++) { + /**准备日志json数据**/ + String logString = logList.get(i); + LinkedHashMap jsonMap = new LinkedHashMap() {{ + put(POD_NAME, podName); + put(NAMESPACE, namespace); + put(MESSAGE, logString); + put(TIMESTAMP, timestamp); + }}; + + /**添加索引创建对象到bulkRequest**/ + bulkRequest.add(new IndexRequest(INDEX_NAME).source(jsonMap)); + } + + /**通过restHighLevelClient发送http的请求批量创建文档**/ + restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT); + } catch (IOException e) { + LogUtil.error(LogEnum.BIZ_K8S, "LogMonitoringApi.addLogsToEs error:{}", e); + return false; + } + return true; + } + + /** + * 日志查询方法 + * + * @param from 日志查询起始值,初始值为1,表示从第一条日志记录开始查询 + * @param size 日志查询记录数 + * @param logMonitoringBo 日志查询bo + * @return LogMonitoringVO 日志查询结果类 + */ + @Override + public LogMonitoringVO searchLogByResName(int from, int size, LogMonitoringBO logMonitoringBo) { + List logList = new ArrayList<>(); + LogMonitoringVO logMonitoringResult = new LogMonitoringVO(ZERO_LONG, logList); + String namespace = logMonitoringBo.getNamespace(); + String resourceName = logMonitoringBo.getResourceName(); + if (StringUtils.isBlank(resourceName) || StringUtils.isBlank(namespace)) { + LogUtil.error(LogEnum.BIZ_K8S, "LogMonitoringApiImpl.searchLogByResName error: param [resourceName] and [namespace] are required"); + return logMonitoringResult; + } + + Set podNameSet = resourceCache.getPodNameByResourceName(logMonitoringBo.getNamespace(), logMonitoringBo.getResourceName()); + + if (CollectionUtils.isEmpty(podNameSet)) { + return logMonitoringResult; + } + /**遍历podNameSet,根据podName查询日志信息**/ + for (String podName : podNameSet) { + logMonitoringBo.setPodName(podName); + + /**查询ES存储的日志信息**/ + List logs = searchLogInfoByEs(from, size, logMonitoringBo); + + if (!CollectionUtils.isEmpty(logs)) { + logList.addAll(MagicNumConstant.ZERO, logs); + } + } + + logMonitoringResult.setLogs(logList); + logMonitoringResult.setTotalLogs(Long.valueOf(logList.size())); + return logMonitoringResult; + } + + /** + * 日志查询方法 + * + * @param from 日志查询起始值,初始值为1,表示从第一条日志记录开始查询 + * @param size 日志查询记录数 + * @param logMonitoringBo 日志查询bo + * @return LogMonitoringVO 日志查询结果类 + */ + @Override + public LogMonitoringVO searchLogByPodName(int from, int size, LogMonitoringBO logMonitoringBo) { + LogMonitoringVO logMonitoringResult = new LogMonitoringVO(); + List logs = searchLogInfoByEs(from, size, logMonitoringBo); + logMonitoringResult.setLogs(logs); + logMonitoringResult.setTotalLogs(Long.valueOf(logs.size())); + return logMonitoringResult; + + } + + /** + * Pod 日志总量查询方法 + * + * @param logMonitoringBo 日志查询bo + * @return long Pod 产生的日志总量 + */ + @Override + public long searchLogCountByPodName(LogMonitoringBO logMonitoringBo) { + SearchRequest searchRequest = buildSearchRequest(ZERO, ZERO, logMonitoringBo); + try { + /**执行搜索,获得响应结果**/ + return restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT).getHits().getTotalHits().value; + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_K8S, "LogMonitoringApiImpl.searchLogCountByPodName error,param:[logMonitoringBo]={}, error:{}", JSON.toJSONString(logMonitoringBo), e); + return ZERO_LONG; + } + } + + /** + * 得到日志信息String + * + * @param podName Pod名称 + * @param namespace 命名空间 + * @return String 所有日志 + */ + private String getLogInfoString(String podName, String namespace) { + PodResource podResource = kubernetesClient.pods().inNamespace(namespace).withName(podName); + Pod pod = podResource.get(); + if (pod != null) { + /**通过k8s客户端获取具体pod的日志监听类**/ + try { + return podResource.getLog(); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_K8S, "LogMonitoringApi.getLogInfoString error, param:[podName]={}, [namespace]={}, error:{}", podName, namespace, e); + } + } + return null; + } + + + /** + * 从Elasticsearch查询日志 + * + * @param from 日志查询起始值 + * @param size 日志查询记录数 + * @param logMonitoringBo 日志查询bo + * @return List 日志集合 + */ + private List searchLogInfoByEs(int from, int size, LogMonitoringBO logMonitoringBo) { + + List logList = new ArrayList<>(); + + SearchRequest searchRequest = buildSearchRequest(from, size, logMonitoringBo); + /**执行搜索**/ + SearchResponse searchResponse; + try { + searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_K8S, "LogMonitoringApiImpl.searchLogInfoByEs error,param:[logMonitoringBo]={}, error:{}", JSON.toJSONString(logMonitoringBo), e); + return logList; + } + /**获取响应结果**/ + SearchHits hits = searchResponse.getHits(); + + SearchHit[] searchHits = hits.getHits(); + if (searchHits.length == MagicNumConstant.ZERO) { + return logList; + } + + for (SearchHit hit : searchHits) { + /**源文档**/ + Map sourceAsMap = hit.getSourceAsMap(); + /**取出message**/ + String message = (String) sourceAsMap.get(MESSAGE); + message = message.replace(LINEBREAK, BLANK); + + /**拼接日志信息**/ + String logString = LOG_PREFIX + message; + /**添加日志信息到集合**/ + logList.add(logString); + } + return logList; + } + + /** + * 构建搜索请求对象 + * + * @param from 日志查询起始值 + * @param size 日志查询记录数 + * @param logMonitoringBo 日志查询bo + * @return SearchRequest ES搜索请求对象 + */ + private SearchRequest buildSearchRequest(int from, int size, LogMonitoringBO logMonitoringBo) { + + /**处理查询范围参数起始值**/ + from = from <= MagicNumConstant.ZERO ? MagicNumConstant.ZERO : --from; + size = size <= MagicNumConstant.ZERO || size > TEN_THOUSAND ? TEN_THOUSAND : size; + + /**创建搜索请求对象**/ + SearchRequest searchRequest = new SearchRequest(); + searchRequest.indices(INDEX_NAME); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.trackTotalHits(true).from(from).size(size); + + /**根据时间戳排序**/ + searchSourceBuilder.sort(TIMESTAMP, SortOrder.ASC); + /**过虑源字段**/ + String[] sourceFieldArray = sourceField.split(COMMA); + + searchSourceBuilder.fetchSource(sourceFieldArray, new String[]{}); + + /**创建布尔查询对象**/ + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + + /**添加podName查询条件**/ + String podName = logMonitoringBo.getPodName(); + if (StringUtils.isNotEmpty(podName)) { + boolQueryBuilder.filter(QueryBuilders.matchQuery(POD_NAME_KEY, podName)); + } + /**添加namespace查询条件**/ + String namespace = logMonitoringBo.getNamespace(); + if (StringUtils.isNotEmpty(namespace)) { + boolQueryBuilder.filter(QueryBuilders.matchQuery(NAMESPACE_KEY, namespace)); + } + /**添加关键字查询条件**/ + String logKeyword = logMonitoringBo.getLogKeyword(); + if (StringUtils.isNotEmpty(logKeyword)) { + boolQueryBuilder.filter(QueryBuilders.matchQuery(MESSAGE, logKeyword).operator(Operator.AND)); + } + /**添加时间范围查询条件**/ + Long beginTimeMillis = logMonitoringBo.getBeginTimeMillis(); + Long endTimeMillis = logMonitoringBo.getEndTimeMillis(); + if (beginTimeMillis != null || endTimeMillis != null){ + beginTimeMillis = beginTimeMillis == null ? ZERO_LONG : beginTimeMillis; + endTimeMillis = endTimeMillis == null ? System.currentTimeMillis() : endTimeMillis; + + /**将毫秒值转换为UTC时间**/ + String beginUtcTime = TimeTransferUtil.dateTransferToUtc(new Date(beginTimeMillis)); + String endUtcTime = TimeTransferUtil.dateTransferToUtc(new Date(endTimeMillis)); + boolQueryBuilder.filter(QueryBuilders.rangeQuery(TIMESTAMP).gte(beginUtcTime).lte(endUtcTime)); + } + + + /**设置boolQueryBuilder到searchSourceBuilder**/ + searchSourceBuilder.query(boolQueryBuilder); + + return searchRequest.source(searchSourceBuilder); + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/MetricsApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/MetricsApiImpl.java new file mode 100644 index 0000000..b4f1e9d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/MetricsApiImpl.java @@ -0,0 +1,498 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.metrics.v1beta1.ContainerMetrics; +import io.fabric8.kubernetes.api.model.metrics.v1beta1.NodeMetricsList; +import io.fabric8.kubernetes.api.model.metrics.v1beta1.PodMetrics; +import io.fabric8.kubernetes.api.model.metrics.v1beta1.PodMetricsList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.functional.StringFormat; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.MetricsApi; +import org.dubhe.k8s.api.PodApi; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.bo.PrometheusMetricBO; +import org.dubhe.k8s.domain.dto.PodQueryDTO; +import org.dubhe.k8s.domain.resource.BizContainer; +import org.dubhe.k8s.domain.resource.BizPod; +import org.dubhe.k8s.domain.resource.BizQuantity; +import org.dubhe.k8s.domain.vo.PodRangeMetricsVO; +import org.dubhe.k8s.domain.vo.PtContainerMetricsVO; +import org.dubhe.k8s.domain.vo.PtNodeMetricsVO; +import org.dubhe.k8s.domain.vo.PtPodsVO; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.PrometheusUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description metrics api + * @date 2020-05-22 + */ +public class MetricsApiImpl implements MetricsApi { + private KubernetesClient client; + + @Autowired + private PodApi podApi; + /** + * prometheus 地址 + */ + @Value("${k8s.prometheus.url}") + private String k8sPrometheusUrl; + /** + * prometheus 查询接口 + */ + @Value("${k8s.prometheus.query}") + private String k8sPrometheusQuery; + /** + * prometheus 范围查询接口 + */ + @Value("${k8s.prometheus.query-range}") + private String k8sPrometheusQueryRange; + /** + * prometheus gpu指标查询参数 + */ + @Value("${k8s.prometheus.gpu-query-param}") + private String k8sPrometheusGpuQueryParam; + /** + * prometheus cpu指标范围查询参数 + */ + @Value("${k8s.prometheus.cpu-range-query-param}") + private String k8sPrometheusCpuRangeQueryParam; + /** + * prometheus 内存指标范围查询参数 + */ + @Value("${k8s.prometheus.mem-range-query-param}") + private String k8sPrometheusMemRangeQueryParam; + /** + * prometheus gpu指标范围查询参数 + */ + @Value("${k8s.prometheus.gpu-range-query-param}") + private String k8sPrometheusGpuRangeQueryParam; + + public MetricsApiImpl(K8sUtils k8sUtils) { + this.client = k8sUtils.getClient(); + } + + /** + * 获取k8s所有节点当前cpu、内存用量 + * + * @return List NodeMetrics 结果类集合 + */ + @Override + public List getNodeMetrics() { + try { + List list = new ArrayList<>(); + NodeMetricsList nodeMetricList = client.top().nodes().metrics(); + nodeMetricList.getItems().forEach(nodeMetrics -> + list.add(new PtNodeMetricsVO(nodeMetrics.getMetadata().getName(), + nodeMetrics.getTimestamp(), + nodeMetrics.getUsage().get(K8sParamConstants.QUANTITY_CPU_KEY).getAmount(), + nodeMetrics.getUsage().get(K8sParamConstants.QUANTITY_CPU_KEY).getFormat(), + nodeMetrics.getUsage().get(K8sParamConstants.QUANTITY_MEMORY_KEY).getAmount(), + nodeMetrics.getUsage().get(K8sParamConstants.QUANTITY_MEMORY_KEY).getFormat() + )) + ); + return list; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "MetricsApiImpl.getNodeMetrics error:{}", e); + return Collections.EMPTY_LIST; + } + } + + /** + * 获取k8s所有Pod资源用量的实时信息 + * + * @return List Pod资源用量结果类集合 + */ + @Override + public List getPodsMetricsRealTime() { + /**查询所有pod的信息一些信息**/ + PodMetricsList metrics = client.top().pods().metrics(); + List list = new ArrayList<>(); + /**将Pod和podName形成映射关系**/ + Map> listMap = client.pods().inAnyNamespace().list().getItems().parallelStream().map(obj -> BizConvertUtils.toBizPod(obj)).collect(Collectors.groupingBy(BizPod::getName)); + if(null == listMap) { + return list; + } + metrics.getItems().stream().forEach(metric -> { + /**创建集合保存PtPodsResult信息**/ + List containers = metric.getContainers(); + containers.stream().forEach(containerMetrics -> { + Map usage = containerMetrics.getUsage(); + PtPodsVO ptContainerMetricsResult = new PtPodsVO(metric.getMetadata().getNamespace(),metric.getMetadata().getName(), + usage.get(K8sParamConstants.QUANTITY_CPU_KEY).getAmount(), + usage.get(K8sParamConstants.QUANTITY_CPU_KEY).getFormat(), + usage.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getAmount(), + usage.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getFormat(), + listMap.get(metric.getMetadata().getName()).get(0).getNodeName(), + listMap.get(metric.getMetadata().getName()).get(0).getPhase(), null); + + List containerList = listMap.get(metric.getMetadata().getName()).get(0).getContainers(); + countGpuUsed(containerList,ptContainerMetricsResult); + list.add(ptContainerMetricsResult); + }); + }); + return list; + } + + /** + * 获取k8s所有pod当前cpu、内存用量的实时使用情况 + * + * @return List Pod资源用量结果类集合 + */ + @Override + public List getPodMetricsRealTime() { + try { + List list = new ArrayList<>(); + Map podNode = new HashMap<>(); + PodMetricsList metrics = client.top().pods().metrics(); + List bizPodList = client.pods().inAnyNamespace().list().getItems().parallelStream().map(obj -> BizConvertUtils.toBizPod(obj)).collect(Collectors.toList()); + bizPodList.stream().forEach(bizPod -> podNode.put(bizPod.getName(), bizPod.getNodeName())); + metrics.getItems().stream().forEach(metric -> { + for (BizPod bizPod : bizPodList) { + if (bizPod.getName().equals(metric.getMetadata().getName())) { + List containers = metric.getContainers(); + containers.stream().forEach(containerMetrics -> + { + Map usage = containerMetrics.getUsage(); + PtPodsVO ptContainerMetricsResult = new PtPodsVO(metric.getMetadata().getNamespace(),metric.getMetadata().getName(), + usage.get(K8sParamConstants.QUANTITY_CPU_KEY).getAmount(), + usage.get(K8sParamConstants.QUANTITY_CPU_KEY).getFormat(), + usage.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getAmount(), + usage.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getFormat(), + podNode.get(metric.getMetadata().getName()), + bizPod.getPhase(), null + ); + List containerList = bizPod.getContainers(); + countGpuUsed(containerList,ptContainerMetricsResult); + list.add(ptContainerMetricsResult); + }); + } + } + + }); + + return list; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "MetricsApiImpl.getPodMetricsRealTime error:{}", e); + return Collections.EMPTY_LIST; + } + } + + /** + * 计算GPU使用数量 + * + * @param containerList BizContainer对象 + * @param ptContainerMetricsResult 封装pod信息 + */ + private void countGpuUsed(List containerList,PtPodsVO ptContainerMetricsResult){ + for (BizContainer container : containerList) { + Map limits = container.getLimits(); + if (limits == null) { + ptContainerMetricsResult.setGpuUsed(SymbolConstant.ZERO); + } else { + BizQuantity bizQuantity = limits.get(K8sParamConstants.GPU_RESOURCE_KEY); + String count = bizQuantity != null ? bizQuantity.getAmount() : SymbolConstant.ZERO; + /**将显卡数量保存起来**/ + ptContainerMetricsResult.setGpuUsed(count); + } + } + } + + /** + * 获取k8s resourceName 下pod当前cpu、内存用量的实时使用情况 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return List Pod资源用量结果类集合 + */ + @Override + public List getPodMetricsRealTime(String namespace, String resourceName) { + List ptPodsVOS = new ArrayList<>(); + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceName)){ + return ptPodsVOS; + } + List pods = podApi.getListByResourceName(namespace,resourceName); + if (CollectionUtils.isEmpty(pods)){ + return ptPodsVOS; + } + List podMetricsList = client.top().pods().metrics(namespace).getItems(); + if (!CollectionUtils.isEmpty(pods)){ + Map podMetricsMap = podMetricsList.stream().collect(Collectors.toMap(obj -> obj.getMetadata().getName(), obj -> obj)); + for (BizPod pod : pods){ + List ptPodsVOList = getPtPodsVO(pod,podMetricsMap.get(pod.getName())); + if (!CollectionUtils.isEmpty(ptPodsVOList)){ + ptPodsVOS.addAll(ptPodsVOList); + } + } + } + for (PtPodsVO ptPodsVO : ptPodsVOS){ + generateGpuUsage(ptPodsVO); + ptPodsVO.calculationPercent(); + } + return ptPodsVOS; + } + + /** + * 获取k8s pod当前cpu、内存用量的实时使用情况 + * @param namespace 命名空间 + * @param podName pod名称 + * @return List Pod资源用量结果类集合 + */ + @Override + public List getPodMetricsRealTimeByPodName(String namespace, String podName) { + List ptPodsVOS = new ArrayList<>(); + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(podName)){ + return ptPodsVOS; + } + BizPod pod = podApi.get(namespace,podName); + if (null == pod){ + return ptPodsVOS; + } + PodMetrics podMetrics = null; + try{ + podMetrics = client.top().pods().metrics(namespace,podName); + }catch (KubernetesClientException e){ + LogUtil.error(LogEnum.BIZ_K8S, "MetricsApiImpl.getPodMetricsRealTimeByPodName error:{}", e); + } + ptPodsVOS = getPtPodsVO(pod,podMetrics); + for (PtPodsVO ptPodsVO : ptPodsVOS){ + generateGpuUsage(ptPodsVO); + ptPodsVO.calculationPercent(); + } + return ptPodsVOS; + } + + /** + * 获取k8s pod当前cpu、内存用量的实时使用情况 + * @param namespace 命名空间 + * @param podNames pod名称列表 + * @return List Pod资源用量结果类集合 + */ + @Override + public List getPodMetricsRealTimeByPodName(String namespace, List podNames) { + List ptPodsVOS = new ArrayList<>(); + for (String podName : podNames){ + ptPodsVOS.addAll(getPodMetricsRealTimeByPodName(namespace,podName)); + } + return ptPodsVOS; + } + + /** + * 获取k8s resourceName 下pod 时间范围内cpu、内存用量的实时使用情况 + * @param podQueryDTO Pod基础信息查询入参 + * @return List Pod监控指标 列表 + */ + @Override + public List getPodRangeMetrics(PodQueryDTO podQueryDTO) { + List podRangeMetricsVOS = new ArrayList<>(); + if (StringUtils.isEmpty(podQueryDTO.getNamespace()) || StringUtils.isEmpty(podQueryDTO.getResourceName())){ + return podRangeMetricsVOS; + } + List pods = podApi.getListByResourceName(podQueryDTO.getNamespace(),podQueryDTO.getResourceName()); + if (CollectionUtils.isEmpty(pods)){ + return podRangeMetricsVOS; + } + podQueryDTO.generateDefaultParam(); + for (BizPod pod : pods){ + podRangeMetricsVOS.add(getPodRangeMetricsVO(pod,podQueryDTO)); + } + return podRangeMetricsVOS; + } + + /** + * 根据namespace、podName获取k8s pod 时间范围内cpu、内存用量的实时使用情况 + * @param podQueryDTO Pod基础信息查询入参 + * @return List Pod监控指标 列表 + */ + @Override + public List getPodRangeMetricsByPodName(PodQueryDTO podQueryDTO) { + List podRangeMetricsVOS = new ArrayList<>(); + if (StringUtils.isEmpty(podQueryDTO.getNamespace()) || CollectionUtils.isEmpty(podQueryDTO.getPodNames())){ + return podRangeMetricsVOS; + } + List pods = podApi.get(podQueryDTO.getNamespace(),podQueryDTO.getPodNames()); + if (null == pods){ + return podRangeMetricsVOS; + } + podQueryDTO.generateDefaultParam(); + for (BizPod pod : pods){ + podRangeMetricsVOS.add(getPodRangeMetricsVO(pod,podQueryDTO)); + } + return podRangeMetricsVOS; + } + + /** + * 组装Pod历史监控指标 + * + * @param pod pod业务类 + * @param podQueryDTO 查询参数 + * @return PodRangeMetricsVO Pod历史监控指标 VO + */ + private PodRangeMetricsVO getPodRangeMetricsVO(BizPod pod,PodQueryDTO podQueryDTO){ + PodRangeMetricsVO podRangeMetricsVO = new PodRangeMetricsVO(pod.getName()); + PrometheusMetricBO cpuRangeMetrics = PrometheusUtil.getQuery(k8sPrometheusUrl+k8sPrometheusQueryRange,PrometheusUtil.getQueryParamMap(k8sPrometheusCpuRangeQueryParam,pod.getName(),podQueryDTO)); + PrometheusMetricBO memRangeMetrics = PrometheusUtil.getQuery(k8sPrometheusUrl+k8sPrometheusQueryRange,PrometheusUtil.getQueryParamMap(k8sPrometheusMemRangeQueryParam,pod.getName(),podQueryDTO)); + PrometheusMetricBO gpuRangeMetrics = PrometheusUtil.getQuery(k8sPrometheusUrl+k8sPrometheusQueryRange,PrometheusUtil.getQueryParamMap(k8sPrometheusGpuRangeQueryParam,pod.getName(),podQueryDTO)); + + StringFormat cpuMetricsFormat = (value)->{ + return value == null ? String.valueOf(MagicNumConstant.ZERO) : NumberUtil.round(Double.valueOf(value.toString()), MagicNumConstant.TWO).toString(); + }; + podRangeMetricsVO.setCpuMetrics(cpuRangeMetrics.getValues(cpuMetricsFormat)); + + StringFormat memMetricsFormat = (value)->{ + return NumberUtil.isNumber(String.valueOf(value)) ? String.valueOf(Long.valueOf(String.valueOf(value)) / MagicNumConstant.BINARY_TEN_EXP) : String.valueOf(MagicNumConstant.ZERO); + }; + podRangeMetricsVO.setMemoryMetrics(memRangeMetrics.getValues(memMetricsFormat)); + podRangeMetricsVO.setGpuMetrics(gpuRangeMetrics.getResults()); + return podRangeMetricsVO; + } + + /** + * 查询Gpu使用率 + * @param ptPodsVO pod信息 + */ + private void generateGpuUsage(PtPodsVO ptPodsVO){ + PrometheusMetricBO prometheusMetricBO = PrometheusUtil.getQuery(k8sPrometheusUrl+k8sPrometheusQuery, PrometheusUtil.getQueryParamMap(k8sPrometheusGpuQueryParam,ptPodsVO.getPodName())); + if (prometheusMetricBO == null){ + return; + } + ptPodsVO.setGpuUsagePersent(prometheusMetricBO.getGpuUsage()); + } + + /** + * assemble PtPodsVO + * @param bizPod pod + * @param metric 查询指标 + * @return List pod信息列表 + */ + private List getPtPodsVO(BizPod bizPod,PodMetrics metric){ + List ptPodsVOList = new ArrayList<>(); + if (metric == null){ + return ptPodsVOList; + } + Map containerMetricsMap = metric.getContainers().stream().collect(Collectors.toMap(obj -> obj.getName(), obj -> obj)); + for (BizContainer container : bizPod.getContainers()){ + Map request = container.getRequests(); + if (containerMetricsMap.get(container.getName()) == null){ + continue; + } + Map usage = containerMetricsMap.get(container.getName()).getUsage(); + PtPodsVO ptContainerMetricsResult = new PtPodsVO(metric.getMetadata().getNamespace(),metric.getMetadata().getName(), + request.get(K8sParamConstants.QUANTITY_CPU_KEY) ==null ? null : request.get(K8sParamConstants.QUANTITY_CPU_KEY).getAmount(), + usage.get(K8sParamConstants.QUANTITY_CPU_KEY).getAmount(), + request.get(K8sParamConstants.QUANTITY_CPU_KEY) ==null ? null : request.get(K8sParamConstants.QUANTITY_CPU_KEY).getFormat(), + usage.get(K8sParamConstants.QUANTITY_CPU_KEY).getFormat(), + request.get(K8sParamConstants.QUANTITY_MEMORY_KEY) == null ? null : request.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getAmount(), + usage.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getAmount(), + request.get(K8sParamConstants.QUANTITY_MEMORY_KEY) == null ? null : request.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getFormat(), + usage.get(K8sParamConstants.QUANTITY_MEMORY_KEY).getFormat(), + bizPod.getNodeName(), + bizPod.getPhase(), null + ); + + Map limits = container.getLimits(); + if (limits == null) { + ptContainerMetricsResult.setGpuUsed(SymbolConstant.ZERO); + } else { + BizQuantity bizQuantity = limits.get(K8sParamConstants.GPU_RESOURCE_KEY); + String count = bizQuantity != null ? bizQuantity.getAmount() : SymbolConstant.ZERO; + /**将显卡数量保存起来**/ + ptContainerMetricsResult.setGpuUsed(count); + } + ptPodsVOList.add(ptContainerMetricsResult); + }; + + return ptPodsVOList; + } + + /** + * 查询命名空间下所有Pod的cpu和内存使用 + * + * @param namespace 命名空间 + * @return List Pod资源用量结果类集合 + */ + @Override + public List getContainerMetrics(String namespace) { + if(StringUtils.isEmpty(namespace)){ + return Collections.EMPTY_LIST; + } + try { + return getContainerMetrics(client.top().pods().metrics(namespace).getItems()); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "MetricsApiImpl.getContainerMetrics error:{}", e); + return Collections.EMPTY_LIST; + } + } + + /** + * 查询所有命名空间下所有pod的cpu和内存使用 + * + * @return List Pod资源用量结果类集合 + */ + @Override + public List getContainerMetrics() { + try { + return getContainerMetrics(client.top().pods().metrics().getItems()); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "MetricsApiImpl.getContainerMetrics error:{}", e); + return Collections.EMPTY_LIST; + } + } + + /** + * 查询所有命名空间下所有pod的cpu和内存使用 + * + * @param pods + * @return List Pod资源用量结果类集合 + */ + private List getContainerMetrics(List pods) { + try { + List list = new ArrayList<>(); + pods.forEach(podMetrics -> + podMetrics.getContainers().forEach(containerMetrics -> + list.add(new PtContainerMetricsVO(podMetrics.getMetadata().getName(), + containerMetrics.getName(), + podMetrics.getTimestamp(), + containerMetrics.getUsage().get(K8sParamConstants.QUANTITY_CPU_KEY).getAmount(), + containerMetrics.getUsage().get(K8sParamConstants.QUANTITY_CPU_KEY).getFormat(), + containerMetrics.getUsage().get(K8sParamConstants.QUANTITY_MEMORY_KEY).getAmount(), + containerMetrics.getUsage().get(K8sParamConstants.QUANTITY_MEMORY_KEY).getFormat())) + ) + ); + return list; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "MetricsApiImpl.getContainerMetrics error, param:{} error:{}", pods, e); + return Collections.EMPTY_LIST; + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelOptJobApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelOptJobApiImpl.java new file mode 100644 index 0000000..acd5939 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelOptJobApiImpl.java @@ -0,0 +1,432 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.google.common.collect.Maps; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import io.fabric8.kubernetes.api.model.batch.Job; +import io.fabric8.kubernetes.api.model.batch.JobBuilder; +import io.fabric8.kubernetes.api.model.batch.JobList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.ModelOptJobApi; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.api.PersistentVolumeClaimApi; +import org.dubhe.k8s.api.ResourceIisolationApi; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtModelOptimizationJobBO; +import org.dubhe.k8s.domain.bo.PtMountDirBO; +import org.dubhe.k8s.domain.bo.PtPersistentVolumeClaimBO; +import org.dubhe.k8s.domain.resource.BizJob; +import org.dubhe.k8s.domain.resource.BizPersistentVolumeClaim; +import org.dubhe.k8s.enums.ImagePullPolicyEnum; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.LackOfResourcesEnum; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; +import org.dubhe.k8s.enums.RestartPolicyEnum; +import org.dubhe.k8s.enums.ShellCommandEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.dubhe.k8s.utils.YamlUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @description ModelOptJobApi 实现类 + * @date 2020-05-29 + */ +public class ModelOptJobApiImpl implements ModelOptJobApi { + private K8sUtils k8sUtils; + private KubernetesClient client; + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + @Autowired + private NodeApi nodeApi; + @Autowired + private PersistentVolumeClaimApi persistentVolumeClaimApi; + @Autowired + private ResourceQuotaApi resourceQuotaApi; + @Autowired + private ResourceIisolationApi resourceIisolationApi; + + public ModelOptJobApiImpl(K8sUtils k8sUtils) { + this.k8sUtils = k8sUtils; + this.client = k8sUtils.getClient(); + } + + /** + * 创建模型优化 Job + * + * @param bo 模型优化 Job BO + * @return BizJob Job 业务类 + */ + @Override + public BizJob create(PtModelOptimizationJobBO bo) { + try { + LimitsOfResourcesEnum limitsOfResources = resourceQuotaApi.reachLimitsOfResources(bo.getNamespace(), bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LimitsOfResourcesEnum.ADEQUATE.equals(limitsOfResources)) { + return new BizJob().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), limitsOfResources.getMessage()); + } + LackOfResourcesEnum lack = nodeApi.isAllocatable(bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LackOfResourcesEnum.ADEQUATE.equals(lack)) { + return new BizJob().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), lack.getMessage()); + } + LogUtil.info(LogEnum.BIZ_K8S, "Params of creating Job--create:{}", bo); + if (!fileStoreApi.createDirs(bo.getDirList().toArray(new String[MagicNumConstant.ZERO]))) { + return new BizJob().error(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + BizJob result = new JobDeployer(bo).buildVolumes().deploy(); + LogUtil.info(LogEnum.BIZ_K8S, "Return value of creating Job--create:{}", result); + return result; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ModelOptJobApiImpl.create error, param:{} error:{}", bo, e); + return new BizJob().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 通过命名空间和资源名称查找Job资源 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizJob Job 业务类 + */ + @Override + public BizJob getWithResourceName(String namespace, String resourceName) { + if (StringUtils.isEmpty(namespace)) { + return new BizJob().baseErrorBadRequest(); + } + JobList bizJobList = client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if (CollectionUtil.isEmpty(bizJobList.getItems())) { + return new BizJob().error(K8sResponseEnum.NOT_FOUND.getCode(), K8sResponseEnum.NOT_FOUND.getMessage()); + } + Job job = bizJobList.getItems().get(0); + return BizConvertUtils.toBizJob(job); + } + + /** + * 通过命名空间查找Job资源 + * + * @param namespace 命名空间 + * @return List Job 业务类集合 + */ + @Override + public List getWithNamespace(String namespace) { + List bizJobList = new ArrayList<>(); + JobList jobList = client.batch().jobs().inNamespace(namespace).list(); + if (CollectionUtil.isEmpty(jobList.getItems())) { + return bizJobList; + } + return BizConvertUtils.toBizJobList(jobList.getItems()); + } + + /** + * 查询所有Job资源 + * + * @return List Job 业务类集合 + */ + @Override + public List listAll() { + return client.batch().jobs().inAnyNamespace().list() + .getItems().parallelStream().map(obj -> BizConvertUtils.toBizJob(obj)).collect(Collectors.toList()); + } + + /** + * 通过命名空间和资源名删除Job + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult deleteByResourceName(String namespace, String resourceName) { + LogUtil.info(LogEnum.BIZ_K8S, "Param of deleteByResourceName namespace {} resourceName {}", namespace,resourceName); + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceName)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ModelOptJobApiImpl.deleteByResourceName error, param:[namespace]={}, [resourceName]={}, error:{}", namespace, resourceName, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + private class JobDeployer { + private String baseName; + private String jobName; + + private String namespace; + private String image; + + private List cmdLines; + + private Map fsMounts; + + private Map resourcesLimitsMap; + private Map baseLabels; + private List volumeMounts; + private List volumes; + private String businessLabel; + private Integer gpuNum; + + private String errCode; + private String errMessage; + + private JobDeployer(PtModelOptimizationJobBO bo) { + this.baseName = bo.getName(); + this.jobName = StrUtil.format(K8sParamConstants.RESOURCE_NAME_TEMPLATE, baseName, RandomUtil.randomString(MagicNumConstant.EIGHT)); + this.namespace = bo.getNamespace(); + this.image = bo.getImage(); + this.cmdLines = new ArrayList(); + this.gpuNum = bo.getGpuNum(); + Optional.ofNullable(bo.getCmdLines()).ifPresent(v -> cmdLines = v); + + this.resourcesLimitsMap = Maps.newHashMap(); + Optional.ofNullable(bo.getCpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_CPU_KEY, new Quantity(v.toString(), K8sParamConstants.CPU_UNIT))); + Optional.ofNullable(bo.getGpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.GPU_RESOURCE_KEY, new Quantity(v.toString()))); + Optional.ofNullable(bo.getMemNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_MEMORY_KEY, new Quantity(v.toString(), K8sParamConstants.MEM_UNIT))); + this.businessLabel = bo.getBusinessLabel(); + this.fsMounts = bo.getFsMounts(); + this.baseLabels = LabelUtils.getBaseLabels(baseName, businessLabel); + + this.volumeMounts = new ArrayList<>(); + this.volumes = new ArrayList<>(); + this.errCode = K8sResponseEnum.SUCCESS.getCode(); + this.errMessage = SymbolConstant.BLANK; + } + + /** + * 部署Job + * + * @return BizJob Job业务类 + */ + public BizJob deploy() { + if (!K8sResponseEnum.SUCCESS.getCode().equals(errCode)) { + return new BizJob().error(errCode, errMessage); + } + //部署job + try { + Job job = deployJob(); + return BizConvertUtils.toBizJob(job); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ModelOptJobApiImpl.deploy error:{}", e); + return new BizJob().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 挂载存储 + * + * @return JobDeployer Job 部署类 + */ + private JobDeployer buildVolumes() { + if (CollectionUtil.isNotEmpty(fsMounts)) { + int i = MagicNumConstant.ZERO; + for (Map.Entry mount : fsMounts.entrySet()) { + boolean availableMount = (mount != null && StringUtils.isNotEmpty(mount.getKey()) && mount.getValue() != null && StringUtils.isNotEmpty(mount.getValue().getDir())); + if (availableMount) { + boolean success = mount.getValue().isRecycle() ? buildFsPvcVolumes(mount.getKey(), mount.getValue(), i) : buildFsVolumes(mount.getKey(), mount.getValue(), i); + if (!success) { + break; + } + i++; + } + } + } + return this; + } + + /** + * 挂载存储 + * + * @param mountPath 挂载路径 + * @param dirBO 挂载路径参数 + * @param i 名称序号 + * @return boolean true成功 false失败 + */ + private boolean buildFsVolumes(String mountPath, PtMountDirBO dirBO, int i) { + volumeMounts.add(new VolumeMountBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX + i) + .withMountPath(mountPath) + .withReadOnly(dirBO.isReadOnly()) + .build()); + volumes.add(new VolumeBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX + i) + .withNewHostPath() + .withPath(dirBO.getDir()) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + return true; + } + + /** + * 按照存储资源声明挂载存储 + * + * @param mountPath 挂载路径 + * @param dirBO 挂载路径参数 + * @param i 名称序号 + * @return boolean true成功 false失败 + */ + private boolean buildFsPvcVolumes(String mountPath, PtMountDirBO dirBO, int i) { + BizPersistentVolumeClaim bizPersistentVolumeClaim = persistentVolumeClaimApi.createWithFsPv(new PtPersistentVolumeClaimBO(namespace, baseName, dirBO)); + if (bizPersistentVolumeClaim.isSuccess()) { + volumeMounts.add(new VolumeMountBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX + i) + .withMountPath(mountPath) + .withReadOnly(dirBO.isReadOnly()) + .build()); + volumes.add(new VolumeBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX + i) + .withNewPersistentVolumeClaim(bizPersistentVolumeClaim.getName(), dirBO.isReadOnly()) + .build()); + return true; + } else { + this.errCode = bizPersistentVolumeClaim.getCode(); + this.errMessage = bizPersistentVolumeClaim.getMessage(); + } + return false; + } + + /** + * 检查是否已经存在,存在则返回 + * + * @return Job 任务类 + */ + private Job alreadyHaveJob() { + JobList list = client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + if (CollectionUtil.isNotEmpty(list.getItems())) { + Job job = list.getItems().get(0); + LogUtil.info(LogEnum.BIZ_K8S, "Skip creating job, {} already exists", job.getMetadata().getName()); + return job; + } + return null; + } + + /** + * 部署Job + * + * @return Job 任务job类 + */ + private Job deployJob() { + //已经存在直接返回 + Job job = alreadyHaveJob(); + if (job != null) { + return job; + } + job = buildJob(); + resourceIisolationApi.addIisolationInfo(job); + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(job)); + job = client.batch().jobs().inNamespace(namespace).create(job); + return job; + } + + /** + * 构建Job + * + * @return Job 任务job类 + */ + private Job buildJob() { + Map childLabels = LabelUtils.getChildLabels(baseName, jobName, K8sKindEnum.JOB.getKind(), businessLabel); + return new JobBuilder() + .withNewMetadata() + .withName(jobName) + .addToLabels(baseLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .withBackoffLimit(0) + .withParallelism(1) + .withNewTemplate() + .withNewMetadata() + .withName(jobName) + .addToLabels(childLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .addToNodeSelector(gpuSelector()) + .addToContainers(buildContainer()) + .addToVolumes(volumes.toArray(new Volume[0])) + .withRestartPolicy(RestartPolicyEnum.NEVER.getRestartPolicy()) + .endSpec() + .endTemplate() + .endSpec() + .build(); + } + + /** + * 添加Gpu label + * + * @return Map Gpu label键值 + */ + private Map gpuSelector() { + Map gpuSelector = new HashMap<>(2); + if (gpuNum != null && gpuNum > 0) { + gpuSelector.put(K8sLabelConstants.NODE_GPU_LABEL_KEY, K8sLabelConstants.NODE_GPU_LABEL_VALUE); + } + return gpuSelector; + } + + /** + * 构建Container + * + * @return Container 容器类 + */ + private Container buildContainer() { + return new ContainerBuilder() + .withNewName(jobName) + .withNewImage(image) + .withNewImagePullPolicy(ImagePullPolicyEnum.IFNOTPRESENT.getPolicy()) + .withVolumeMounts(volumeMounts) + .addNewCommand(ShellCommandEnum.BASH.getShell()) + .addAllToArgs(cmdLines) + .withNewResources().addToLimits(resourcesLimitsMap).endResources() + .build(); + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelServingApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelServingApiImpl.java new file mode 100644 index 0000000..5169171 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ModelServingApiImpl.java @@ -0,0 +1,337 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.google.common.collect.Maps; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerPort; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.LabelSelector; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretList; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceList; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentList; +import io.fabric8.kubernetes.api.model.extensions.Ingress; +import io.fabric8.kubernetes.api.model.extensions.IngressList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.ModelServingApi; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.api.PodApi; +import org.dubhe.k8s.api.ResourceIisolationApi; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.api.VolumeApi; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.BuildIngressBO; +import org.dubhe.k8s.domain.bo.BuildFsVolumeBO; +import org.dubhe.k8s.domain.bo.BuildServiceBO; +import org.dubhe.k8s.domain.bo.ModelServingBO; +import org.dubhe.k8s.domain.vo.ModelServingVO; +import org.dubhe.k8s.domain.vo.VolumeVO; +import org.dubhe.k8s.enums.ImagePullPolicyEnum; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; +import org.dubhe.k8s.enums.RestartPolicyEnum; +import org.dubhe.k8s.enums.ShellCommandEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.dubhe.k8s.utils.ResourceBuildUtils; +import org.dubhe.k8s.utils.YamlUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * @description 模型部署接口实现 + * @date 2020-09-10 + */ +public class ModelServingApiImpl implements ModelServingApi { + private K8sUtils k8sUtils; + private KubernetesClient client; + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + @Autowired + private VolumeApi volumeApi; + @Autowired + private NodeApi nodeApi; + @Autowired + private PodApi podApi; + @Autowired + private ResourceQuotaApi resourceQuotaApi; + @Autowired + private ResourceIisolationApi resourceIisolationApi; + + @Value("${k8s.serving.host}") + String servingHost; + + @Value("${k8s.serving.tls-crt}") + String servingTlsCrt; + + @Value("${k8s.serving.tls-key}") + String servingTlsKey; + + private static final String MODEL_SERVING_MAX_UPLOAD_SIZE = "100m"; + + public ModelServingApiImpl(K8sUtils k8sUtils) { + this.k8sUtils = k8sUtils; + this.client = k8sUtils.getClient(); + } + + /** + * 创建 + * @param bo + * @return + */ + @Override + public ModelServingVO create(ModelServingBO bo) { + try { + //资源配额校验 + LimitsOfResourcesEnum limitsOfResources = resourceQuotaApi.reachLimitsOfResources(bo.getNamespace(), bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LimitsOfResourcesEnum.ADEQUATE.equals(limitsOfResources)) { + return new ModelServingVO().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), limitsOfResources.getMessage()); + } + LogUtil.info(LogEnum.BIZ_K8S, "Params of creating ModelServing--create:{}", bo); + if (!fileStoreApi.createDirs(bo.getDirList().toArray(new String[MagicNumConstant.ZERO]))) { + return new ModelServingVO().error(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + //存储卷构建 + VolumeVO volumeVO = volumeApi.buildFsVolumes(new BuildFsVolumeBO(bo.getNamespace(), bo.getResourceName(), bo.getFsMounts())); + if (!K8sResponseEnum.SUCCESS.getCode().equals(volumeVO.getCode())) { + return new ModelServingVO().error(volumeVO.getCode(), volumeVO.getMessage()); + } + + //名称生成 + String deploymentName = StrUtil.format(K8sParamConstants.RESOURCE_NAME_TEMPLATE, bo.getResourceName(), RandomUtil.randomString(MagicNumConstant.EIGHT)); + String svcName = StrUtil.format(K8sParamConstants.SUB_RESOURCE_NAME_TEMPLATE, bo.getResourceName(), K8sParamConstants.SVC_SUFFIX, RandomUtil.randomString(MagicNumConstant.FIVE)); + String ingressName = StrUtil.format(K8sParamConstants.SUB_RESOURCE_NAME_TEMPLATE, bo.getResourceName(), K8sParamConstants.INGRESS_SUFFIX, RandomUtil.randomString(MagicNumConstant.FIVE)); + + //标签生成 + Map baseLabels = LabelUtils.getBaseLabels(bo.getResourceName(), bo.getBusinessLabel()); + Map podLabels = LabelUtils.getChildLabels(bo.getResourceName(), deploymentName, K8sKindEnum.DEPLOYMENT.getKind(), bo.getBusinessLabel()); + + //部署deployment + Deployment deployment = buildDeployment(bo, volumeVO, deploymentName); + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}, yaml信息为{}", deploymentName, YamlUtils.dumpAsYaml(deployment)); + resourceIisolationApi.addIisolationInfo(deployment); + Deployment deploymentResult = client.apps().deployments().inNamespace(bo.getNamespace()).create(deployment); + + //部署service + BuildServiceBO buildServiceBO = new BuildServiceBO(bo.getNamespace(), svcName, baseLabels, podLabels); + if (bo.getHttpPort() != null) { + buildServiceBO.addPort(ResourceBuildUtils.buildServicePort(bo.getHttpPort(), bo.getHttpPort(), SymbolConstant.HTTP)); + } + if (bo.getGrpcPort() != null) { + buildServiceBO.addPort(ResourceBuildUtils.buildServicePort(bo.getGrpcPort(), bo.getGrpcPort(), SymbolConstant.GRPC)); + } + Service service = ResourceBuildUtils.buildService(buildServiceBO); + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}, yaml信息为{}", svcName, YamlUtils.dumpAsYaml(service)); + Service serviceResult = client.services().create(service); + + //部署ingress + BuildIngressBO buildIngressBO = new BuildIngressBO(bo.getNamespace(), ingressName, baseLabels); + if (StringUtils.isNotEmpty(buildIngressBO.getMaxUploadSize())) { + buildIngressBO.putAnnotation(K8sParamConstants.INGRESS_PROXY_BODY_SIZE_KEY, buildIngressBO.getMaxUploadSize()); + } + if (bo.getHttpPort() != null) { + String httpHost = RandomUtil.randomString(MagicNumConstant.SIX) + SymbolConstant.DOT + servingHost; + buildIngressBO.addIngressRule(ResourceBuildUtils.buildIngressRule(httpHost, svcName, SymbolConstant.HTTP)); + } + Secret secretResult = null; + if (bo.getGrpcPort() != null) { + String secretName = StrUtil.format(K8sParamConstants.SUB_RESOURCE_NAME_TEMPLATE, bo.getResourceName(), SymbolConstant.TOKEN, RandomUtil.randomString(MagicNumConstant.FIVE)); + Map data = new HashMap(MagicNumConstant.FOUR) { + { + put(K8sParamConstants.SECRET_TLS_TLS_CRT, servingTlsCrt); + put(K8sParamConstants.SECRET_TLS_TLS_KEY, servingTlsKey); + } + }; + Secret secret = ResourceBuildUtils.buildTlsSecret(bo.getNamespace(), secretName, baseLabels, data); + secretResult = client.secrets().create(secret); + + String grpcHost = RandomUtil.randomString(MagicNumConstant.SIX) + SymbolConstant.DOT + servingHost; + buildIngressBO.addIngressRule(ResourceBuildUtils.buildIngressRule(grpcHost, svcName, SymbolConstant.GRPC)); + buildIngressBO.addIngressTLS(ResourceBuildUtils.buildIngressTLS(secretName, grpcHost)); + buildIngressBO.putAnnotation(K8sParamConstants.INGRESS_CLASS_KEY, StringConstant.NGINX_LOWERCASE); + buildIngressBO.putAnnotation(K8sParamConstants.INGRESS_SSL_REDIRECT_KEY, StringConstant.TRUE_LOWERCASE); + buildIngressBO.putAnnotation(K8sParamConstants.INGRESS_BACKEND_PROTOCOL_KEY, StringConstant.GRPC_CAPITALIZE); + } + Ingress ingress = ResourceBuildUtils.buildIngress(buildIngressBO); + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}, yaml信息为{}", ingressName, YamlUtils.dumpAsYaml(ingress)); + Ingress ingressResult = client.extensions().ingresses().create(ingress); + + return new ModelServingVO(BizConvertUtils.toBizSecret(secretResult), BizConvertUtils.toBizService(serviceResult), BizConvertUtils.toBizDeployment(deploymentResult), BizConvertUtils.toBizIngress(ingressResult)); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ModelOptJobApiImpl.create error, param:{} error:", bo, e); + return new ModelServingVO().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult delete(String namespace, String resourceName) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "delete model serving namespace:{} resourceName:{}",namespace,resourceName); + DeploymentList deploymentList = client.apps().deployments().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if (deploymentList == null || deploymentList.getItems().size() == 0){ + return new PtBaseResult(); + } + Boolean res = client.extensions().ingresses().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete() + && client.services().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete() + && client.apps().deployments().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete() + && client.secrets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete(); + if (res) { + return new PtBaseResult(); + } else { + return K8sResponseEnum.REPEAT.toPtBaseResult(); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "delete error:", e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 查询 + * @param namespace + * @param resourceName + * @return + */ + @Override + public ModelServingVO get(String namespace, String resourceName) { + try { + IngressList ingressList = client.extensions().ingresses().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + Ingress ingress = CollectionUtil.isEmpty(ingressList.getItems()) ? null : ingressList.getItems().get(0); + ServiceList svcList = client.services().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + Service svc = CollectionUtil.isEmpty(svcList.getItems()) ? null : svcList.getItems().get(0); + DeploymentList deploymentList = client.apps().deployments().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + Deployment deployment = CollectionUtil.isEmpty(deploymentList.getItems()) ? null : deploymentList.getItems().get(0); + SecretList secretList = client.secrets().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + Secret secret = CollectionUtil.isEmpty(secretList.getItems()) ? null : secretList.getItems().get(0); + return new ModelServingVO(BizConvertUtils.toBizSecret(secret), BizConvertUtils.toBizService(svc), BizConvertUtils.toBizDeployment(deployment), BizConvertUtils.toBizIngress(ingress)); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "get error:", e); + return new ModelServingVO().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 构建Deployment + * + * @return Deployment + */ + private Deployment buildDeployment(ModelServingBO bo, VolumeVO volumeVO, String deploymentName) { + Map childLabels = LabelUtils.getChildLabels(bo.getResourceName(), deploymentName, K8sKindEnum.DEPLOYMENT.getKind(), bo.getBusinessLabel()); + LabelSelector labelSelector = new LabelSelector(); + labelSelector.setMatchLabels(childLabels); + return new DeploymentBuilder() + .withNewMetadata() + .withName(deploymentName) + .addToLabels(LabelUtils.getBaseLabels(bo.getResourceName(), bo.getBusinessLabel())) + .withNamespace(bo.getNamespace()) + .endMetadata() + .withNewSpec() + .withReplicas(bo.getReplicas()) + .withSelector(labelSelector) + .withNewTemplate() + .withNewMetadata() + .withName(deploymentName) + .addToLabels(childLabels) + .withNamespace(bo.getNamespace()) + .endMetadata() + .withNewSpec() + .addToNodeSelector(k8sUtils.gpuSelector(bo.getGpuNum())) + .addToContainers(buildContainer(bo, volumeVO, deploymentName)) + .addToVolumes(volumeVO.getVolumes().toArray(new Volume[0])) + .withRestartPolicy(RestartPolicyEnum.ALWAYS.getRestartPolicy()) + .endSpec() + .endTemplate() + .endSpec() + .build(); + } + + /** + * 构建 Container + * @param bo + * @param volumeVO + * @param name + * @return + */ + private Container buildContainer(ModelServingBO bo, VolumeVO volumeVO, String name) { + Map resourcesLimitsMap = Maps.newHashMap(); + Optional.ofNullable(bo.getCpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_CPU_KEY, new Quantity(v.toString(), K8sParamConstants.CPU_UNIT))); + Optional.ofNullable(bo.getGpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.GPU_RESOURCE_KEY, new Quantity(v.toString()))); + Optional.ofNullable(bo.getMemNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_MEMORY_KEY, new Quantity(v.toString(), K8sParamConstants.MEM_UNIT))); + Container container = new ContainerBuilder() + .withNewName(name) + .withNewImage(bo.getImage()) + .withNewImagePullPolicy(ImagePullPolicyEnum.IFNOTPRESENT.getPolicy()) + .withVolumeMounts(volumeVO.getVolumeMounts()) + .withNewResources().addToLimits(resourcesLimitsMap).endResources() + .build(); + if (bo.getCmdLines() != null) { + container.setCommand(Arrays.asList(ShellCommandEnum.BIN_BANSH.getShell())); + container.setArgs(bo.getCmdLines()); + } + List ports = new ArrayList<>(); + if (bo.getHttpPort() != null) { + ports.add(new ContainerPortBuilder() + .withContainerPort(bo.getHttpPort()) + .withName(SymbolConstant.HTTP).build()); + } + if (bo.getGrpcPort() != null) { + ports.add(new ContainerPortBuilder() + .withContainerPort(bo.getGrpcPort()) + .withName(SymbolConstant.GRPC).build()); + } + if (CollectionUtil.isNotEmpty(ports)) { + container.setPorts(ports); + } + return container; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NamespaceApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NamespaceApiImpl.java new file mode 100644 index 0000000..8fe5dc6 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NamespaceApiImpl.java @@ -0,0 +1,332 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import com.alibaba.fastjson.JSON; +import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.api.model.NamespaceList; +import io.fabric8.kubernetes.api.model.ResourceQuota; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.api.NamespaceApi; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.resource.BizNamespace; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.ValidationTypeEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @description NamespaceApi 实现类 + * @date 2020-04-15 + */ +public class NamespaceApiImpl implements NamespaceApi { + + private KubernetesClient client; + + @Autowired + private ResourceQuotaApi resourceQuotaApi; + + @Value("${k8s.namespace-limits.cpu}") + private Integer cpuLimit; + + @Value("${k8s.namespace-limits.memory}") + private Integer memoryLimit; + + @Value("${k8s.namespace-limits.gpu}") + private Integer gpuLimit; + + + + public NamespaceApiImpl(K8sUtils k8sUtils) { + this.client = k8sUtils.getClient(); + } + + /** + * 创建NamespaceLabels,为null则不添加标签 + * + * @param namespace 命名空间 + * @param labels 标签Map + * @return BizNamespace Namespace 业务类 + */ + @Override + public BizNamespace create(@K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) String namespace, Map labels) { + try { + BizNamespace bizNamespace = get(namespace); + if (bizNamespace != null){ + return bizNamespace; + } + Namespace ns = new NamespaceBuilder().withNewMetadata().withName(namespace).addToLabels(LabelUtils.getBaseLabels(namespace, labels)).endMetadata().build(); + Namespace res = client.namespaces().create(ns); + resourceQuotaApi.create(res.getMetadata().getName(),res.getMetadata().getName(),cpuLimit,memoryLimit,gpuLimit); + return BizConvertUtils.toBizNamespace(res); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NamespaceApiImpl.create error, param:[namespace]={}, [labels]={},error:{}",namespace, labels, e); + return new BizNamespace().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 根据namespace查询BizNamespace + * + * @param namespace 命名空间 + * @return BizNamespace Namespace 业务类 + */ + @Override + public BizNamespace get(String namespace) { + if (StringUtils.isEmpty(namespace)) { + return new BizNamespace().baseErrorBadRequest(); + } + return BizConvertUtils.toBizNamespace(client.namespaces().withName(namespace).get()); + } + + /** + * 查询所有的BizNamespace + * + * @return List Namespace 业务类集合 + */ + @Override + public List listAll() { + NamespaceList namespaceList = client.namespaces().list(); + if (namespaceList == null || CollectionUtils.isEmpty(namespaceList.getItems())) { + return Collections.emptyList(); + } + return namespaceList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizNamespace(obj)).collect(Collectors.toList()); + } + + /** + * 根据label标签查询所有的BizNamespace + * + * @param labelKey 标签的键 + * @return List Namespace 业务类集合 + */ + @Override + public List list(String labelKey) { + if (StringUtils.isEmpty(labelKey)) { + return Collections.EMPTY_LIST; + } + NamespaceList namespaceList = client.namespaces().withLabel(labelKey, null).list(); + if (namespaceList == null || CollectionUtils.isEmpty(namespaceList.getItems())) { + return Collections.EMPTY_LIST; + } + return namespaceList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizNamespace(obj)).collect(Collectors.toList()); + } + + /** + * 根据label标签集合查询所有的BizNamespace数据 + * + * @param labels 标签键的集合 + * @return List Namespace业务类集合 + */ + @Override + public List list(Set labels) { + if (CollectionUtils.isEmpty(labels)) { + return Collections.EMPTY_LIST; + } + Map map = new HashMap<>(); + Iterator it = labels.iterator(); + while (it.hasNext()) { + //根据label的key查询,无需关系value值 + map.put(it.next(), null); + } + NamespaceList namespaceList = client.namespaces().withLabels(map).list(); + if (namespaceList == null || CollectionUtils.isEmpty(namespaceList.getItems())) { + return Collections.EMPTY_LIST; + } + return namespaceList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizNamespace(obj)).collect(Collectors.toList()); + } + + /** + * 删除命名空间 + * + * @param namespace 命名空间 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult delete(String namespace) { + LogUtil.info(LogEnum.BIZ_K8S, "Param of delete namespace {}", namespace); + if (StringUtils.isEmpty(namespace)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + if (client.namespaces().withName(namespace).delete()) { + return new PtBaseResult(); + } else { + return K8sResponseEnum.REPEAT.toPtBaseResult(); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NamespaceApiImpl.delete error, param:[namespace]={}, error:{}", namespace, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除命名空间的标签 + * + * @param namespace 命名空间 + * @param labelKey 标签的键 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult removeLabel(String namespace, String labelKey) { + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(labelKey)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + client.namespaces().withName(namespace) + .edit() + .editMetadata() + .removeFromLabels(labelKey) + .endMetadata() + .done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NamespaceApiImpl.removeLabel error, param:[namespace]={}, [labelKey]={}, error:{}", namespace, labelKey, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除命名空间下的多个标签 + * + * @param namespace 命名空间 + * @param labels 标签键的集合 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult removeLabels(String namespace, Set labels) { + if (StringUtils.isEmpty(namespace) || CollectionUtils.isEmpty(labels)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + Map map = new HashMap<>(); + Iterator it = labels.iterator(); + while (it.hasNext()) { + //根据label的key查询,无需关系value值 + map.put(it.next(), null); + } + client.namespaces().withName(namespace) + .edit() + .editMetadata() + .removeFromLabels(map) + .endMetadata() + .done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NamespaceApiImpl.removeLabel error, param:[namespace]={}, [labels]={},error:{}", namespace, labels, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 将labelKey和labelValue添加到指定的命名空间 + * + * @param namespace 命名空间 + * @param labelKey 标签的键 + * @param labelValue 标签的值 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult addLabel(String namespace, String labelKey, String labelValue) { + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(labelKey)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + client.namespaces().withName(namespace).edit().editMetadata().addToLabels(labelKey, labelValue).endMetadata().done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NamespaceApiImpl.addLabel error, param:[namespace]={}, [labelKey]={}, [labelValue]={}, error:{}", namespace, labelKey, labelValue, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 将多个label标签添加到指定的命名空间 + * + * @param namespace 命名空间 + * @param labels 标签 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult addLabels(String namespace, Map labels) { + if (StringUtils.isEmpty(namespace) || CollectionUtils.isEmpty(labels)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + client.namespaces().withName(namespace).edit().editMetadata().addToLabels(labels).endMetadata().done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NamespaceApiImpl.addLabels error, param:[namespace]={}, [labels]={},error:{}",namespace, JSON.toJSONString(labels), e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + + /** + * 命名空间的资源限制 + * + * @param namespace 命名空间 + * @param quota 资源限制参数类 + * @return ResourceQuota 资源限制参数类 + */ + @Override + public ResourceQuota addResourceQuota(String namespace, ResourceQuota quota) { + return client.resourceQuotas().inNamespace(namespace).create(quota); + } + + /** + * 获得命名空间下的所有的资源限制 + * + * @param namespace 命名空间 + * @return List 资源限制参数类 + */ + @Override + public List listResourceQuotas(String namespace) { + return client.resourceQuotas().inNamespace(namespace).list().getItems(); + } + + /** + * 解除对命名空间的资源限制 + * + * @param namespace 命名空间 + * @param quota 资源限制参数类 + * @return boolean true成功 false失败 + */ + @Override + public boolean removeResourceQuota(String namespace, ResourceQuota quota) { + return client.resourceQuotas().inNamespace(namespace).delete(quota); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NativeResourceApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NativeResourceApiImpl.java new file mode 100644 index 0000000..68aa076 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NativeResourceApiImpl.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.api.impl; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.k8s.api.NativeResourceApi; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.biz.log.utils.LogUtil; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * @description 本地资源实现类 + * @date 2020-08-28 + */ +public class NativeResourceApiImpl implements NativeResourceApi { + private KubernetesClient client; + public NativeResourceApiImpl(K8sUtils k8sUtils) { + this.client = k8sUtils.getClient(); + } + + /** + * + * @param crYaml cr定义yaml脚本 + * @return List HasMetadata资源集合 + */ + @Override + public List create(String crYaml) { + LogUtil.info(LogEnum.BIZ_K8S, "Param of create crYaml {}", crYaml); + try { + return client.load(new ByteArrayInputStream(crYaml.getBytes())).deletingExisting().createOrReplace(); + }catch (KubernetesClientException e){ + LogUtil.error(LogEnum.BIZ_K8S, "Create NativeResource error:{} ,yml:{}", e,crYaml); + return new ArrayList<>(); + } + } + + /** + * + * @param crYaml cr定义yaml脚本 + * @return boolean true删除成功 false删除失败 + */ + @Override + public boolean delete(String crYaml) { + LogUtil.info(LogEnum.BIZ_K8S, "Param of delete crYaml {}", crYaml); + try { + return client.load(new ByteArrayInputStream(crYaml.getBytes())).delete(); + }catch (KubernetesClientException e){ + LogUtil.error(LogEnum.BIZ_K8S, "Delete NativeResource error:{} ,yml:{}", e,crYaml); + return false; + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NodeApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NodeApiImpl.java new file mode 100644 index 0000000..f362822 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/NodeApiImpl.java @@ -0,0 +1,723 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import io.fabric8.kubernetes.api.model.Node; +import io.fabric8.kubernetes.api.model.NodeList; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodList; +import io.fabric8.kubernetes.api.model.Taint; +import io.fabric8.kubernetes.api.model.Toleration; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.MetricsApi; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.resource.BizNode; +import org.dubhe.k8s.domain.resource.BizTaint; +import org.dubhe.k8s.domain.vo.PtNodeMetricsVO; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.K8sTolerationEffectEnum; +import org.dubhe.k8s.enums.LackOfResourcesEnum; +import org.dubhe.k8s.enums.PodPhaseEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.ResourceBuildUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description NodeApi实现类 + * @date 2020-04-21 + */ +public class NodeApiImpl implements NodeApi { + + @Autowired + private MetricsApi metricsApi; + + private KubernetesClient client; + + @Autowired + private UserContextService userContextService; + + public NodeApiImpl(K8sUtils k8sUtils) { + this.client = k8sUtils.getClient(); + } + + /** + * 根据节点名称查询节点信息 + * + * @param nodeName 节点名称 + * @return BizNode Node 业务类 + */ + @Override + public BizNode get(String nodeName) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Input nodeName={}", nodeName); + if (StringUtils.isEmpty(nodeName)) { + return null; + } + Node node = client.nodes().withName(nodeName).get(); + BizNode bizNode = BizConvertUtils.toBizNode(node); + LogUtil.info(LogEnum.BIZ_K8S, "Output {}", bizNode); + return bizNode; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.get error, param:[nodeName]={}, error:{}", nodeName, e); + return new BizNode().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 查询所有节点信息 + * + * @return List Node 业务类集合 + */ + @Override + public List listAll() { + try { + NodeList nodes = client.nodes().list(); + if (nodes == null || CollectionUtils.isEmpty(nodes.getItems())) { + return Collections.EMPTY_LIST; + } + List bizNodeList = nodes.getItems().parallelStream().map(obj -> BizConvertUtils.toBizNode(obj).setReady()).collect(Collectors.toList()); + LogUtil.info(LogEnum.BIZ_K8S, "Output {}", bizNodeList); + return bizNodeList; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.listAll error:{}", e); + return Collections.EMPTY_LIST; + } + } + + /** + * 给节点添加单个标签 + * + * @param nodeName 节点名称 + * @param labelKey 标签的键 + * @param labelValue 标签的值 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult addLabel(String nodeName, String labelKey, String labelValue) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Input nodeName={};labelKey={};labelValue={}", nodeName, labelKey, labelValue); + if (StringUtils.isEmpty(nodeName) || StringUtils.isEmpty(labelKey)) { + return new PtBaseResult().baseErrorBadRequest(); + } + client.nodes().withName(nodeName).edit().editMetadata().addToLabels(labelKey, labelValue).endMetadata().done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.addLabel error, param:[nodeName]={}, [labelKey]={}, [labelValue]={}, error:{}", nodeName, labelKey, labelValue, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 给节点添加多个标签 + * + * @param nodeName 节点名称 + * @param labels 标签Map + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult addLabels(String nodeName, Map labels) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Input nodeName={};labels={}", nodeName, labels); + if (StringUtils.isEmpty(nodeName) || CollectionUtils.isEmpty(labels)) { + return new PtBaseResult().baseErrorBadRequest(); + } + client.nodes().withName(nodeName).edit().editMetadata().addToLabels(labels).endMetadata().done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.addLabels error, param:[nodeName]={}, [labels]={},error:{}",nodeName, JSON.toJSONString(labels), e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除节点单个标签 + * + * @param nodeName 节点名称 + * @param labelKey 标签的键 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult deleteLabel(String nodeName, String labelKey) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Input nodeName={};labelKey={}", nodeName, labelKey); + if (StringUtils.isEmpty(nodeName) || StringUtils.isEmpty(labelKey)) { + return new PtBaseResult().baseErrorBadRequest(); + } + client.nodes().withName(nodeName).edit().editMetadata().removeFromLabels(labelKey).endMetadata().done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.deleteLabel error, param:[nodeName]={}, [labelKey]={},error:{}",nodeName, labelKey, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除节点的多个标签 + * + * @param nodeName 节点名称 + * @param labels 标签键集合 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult deleteLabels(String nodeName, Set labels) { + try { + LogUtil.info(LogEnum.BIZ_K8S, "Input nodeName={};labels={}", nodeName, labels); + if (StringUtils.isEmpty(nodeName) || CollectionUtils.isEmpty(labels)) { + return new PtBaseResult().baseErrorBadRequest(); + } + Map map = new HashMap<>(); + Iterator it = labels.iterator(); + while (it.hasNext()) { + map.put(it.next(), null); + } + client.nodes().withName(nodeName).edit().editMetadata().removeFromLabels(map).endMetadata().done(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.deleteLabelS error, param:[nodeName]={}, [labels]={},error:{}", nodeName, JSON.toJSONString(labels), e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 根据标签查询节点 + * + * @param key 标签key + * @param value 标签value + * @return List + */ + @Override + public List getWithLabel(String key, String value) { + try { + List bizNodes = new ArrayList<>(); + if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)){ + return bizNodes; + } + NodeList nodeList = client.nodes().withLabel(key,value).list(); + if (nodeList != null && !CollectionUtils.isEmpty(nodeList.getItems())){ + return BizConvertUtils.toBizNodes(nodeList.getItems()); + } + return bizNodes; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.getWithLabels error, param:[key]={} [value]={},error:{}", key,value, e); + return new ArrayList<>(); + } + } + + /** + * 根据标签查询节点 + * + * @param labels 标签 + * @return List + */ + @Override + public List getWithLabels(Map labels) { + try { + List bizNodes = new ArrayList<>(); + if (CollectionUtils.isEmpty(labels)){ + return bizNodes; + } + NodeList nodeList = client.nodes().withLabels(labels).list(); + if (nodeList != null && !CollectionUtils.isEmpty(nodeList.getItems())){ + return BizConvertUtils.toBizNodes(nodeList.getItems()); + } + return bizNodes; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.getWithLabels error, param:[labels]={},error:{}", JSON.toJSONString(labels), e); + return new ArrayList<>(); + } + } + + /** + * 设置节点是否可调度 + * + * @param nodeName 节点名称 + * @param schedulable 参数true或false + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult schedulable(String nodeName, boolean schedulable) { + LogUtil.info(LogEnum.BIZ_K8S, "Input nodeName={};schedulable={}", nodeName, schedulable); + if (StringUtils.isEmpty(nodeName)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + Node node = client.nodes().withName(nodeName).get(); + if (node == null) { + return K8sResponseEnum.NOT_FOUND.toPtBaseResult(); + } + node.getSpec().setUnschedulable(schedulable); + client.nodes().withName(nodeName).replace(node); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.schedulable error:{}", e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 查询集群资源是否充足 + * + * @param nodeSelector 节点选择标签 + * @param taints 该资源所能容忍的污点 + * @param cpuNum 单位为m 1核等于1000m + * @param memNum 单位为Mi 1Mi等于1024Ki + * @param gpuNum 单位为显卡,即"1"表示1张显卡 + * @return LackOfResourcesEnum 资源缺乏枚举类 + */ + @Override + public LackOfResourcesEnum isAllocatable(Map nodeSelector, List taints, Integer cpuNum, Integer memNum, Integer gpuNum) { + LogUtil.info(LogEnum.BIZ_K8S, "Input nodeSelector={};taints={};cpuNum={};memNum={};gpuNum={}", JSON.toJSONString(nodeSelector), JSON.toJSONString(taints), cpuNum, memNum, gpuNum); + NodeList list; + try { + list = client.nodes().list(); + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.isAllocatable error:{}", e); + return LackOfResourcesEnum.LACK_OF_NODE; + } + + List nodeItems = list.getItems(); + //根据nodeSelector筛选节点 + if (CollectionUtil.isNotEmpty(nodeSelector) && nodeSelector.size() > NumberConstant.NUMBER_1){ + return LackOfResourcesEnum.LACK_OF_NODE; + } else if (CollectionUtil.isNotEmpty(nodeSelector) && nodeSelector.size() == NumberConstant.NUMBER_1){ + for (String nodeSelectorKey : nodeSelector.keySet()) { + String nodeSelectorValue = nodeSelector.get(nodeSelectorKey); + nodeItems = nodeItems.stream().filter(nodeItem -> nodeSelectorValue.equals(nodeItem.getMetadata().getLabels().get(nodeSelectorKey))) + .collect(Collectors.toList()); + } + } + //根据可容忍的污点筛选节点 + if (CollectionUtil.isEmpty(taints)){ + nodeItems = nodeItems.stream().filter(nodeItem -> CollectionUtils.isEmpty(nodeItem.getSpec().getTaints())).collect(Collectors.toList()); + } else { + List taintNodes = new ArrayList<>(); + for (BizTaint taint : taints) { + taintNodes.addAll( nodeItems.stream().filter(nodeItem -> doesTaintExit(nodeItem, taint)).collect(Collectors.toList())); + } + nodeItems = taintNodes; + } + + if (CollectionUtils.isEmpty(nodeItems)) { + return LackOfResourcesEnum.LACK_OF_NODE; + } + if (cpuNum != null && cpuNum >= MagicNumConstant.ZERO) { + nodeItems = isCpuAllocatable(cpuNum, nodeItems); + if (CollectionUtils.isEmpty(nodeItems)) { + return LackOfResourcesEnum.LACK_OF_CPU; + } + } + + if (memNum != null && memNum >= MagicNumConstant.ZERO) { + nodeItems = isMemAllocatable(memNum, nodeItems); + if (CollectionUtils.isEmpty(nodeItems)) { + return LackOfResourcesEnum.LACK_OF_MEM; + } + } + + if (gpuNum != null && gpuNum >= MagicNumConstant.ZERO) { + nodeItems = isGpuAllocatable(gpuNum, nodeItems); + if (CollectionUtils.isEmpty(nodeItems)) { + return LackOfResourcesEnum.LACK_OF_GPU; + } + } + + return LackOfResourcesEnum.ADEQUATE; + } + + /** + * 查询集群资源是否充足 + * + * @param cpuNum 单位为m 1核等于1000m + * @param memNum 单位为Mi 1Mi等于1024Ki + * @param gpuNum 单位为显卡,即"1"表示1张显卡 + * @return LackOfResourcesEnum 资源缺乏枚举类 + */ + @Override + public LackOfResourcesEnum isAllocatable(Integer cpuNum, Integer memNum, Integer gpuNum) { + Toleration toleration = getNodeIsolationToleration(); + if (toleration == null){ + return isAllocatable(null,null,cpuNum,memNum,gpuNum); + }else { + return isAllocatable(getNodeIsolationNodeSelector(), geBizTaintListByUserId(),cpuNum,memNum,gpuNum); + } + } + + /** + * 判断是否超出总可分配gpu数 + * @param gpuNum + * @return LackOfResourcesEnum 资源缺乏枚举类 + */ + @Override + public LackOfResourcesEnum isOutOfTotalAllocatableGpu(Integer gpuNum){ + Integer remainingGpuNum = getTotalGpuNum() - getAllocatedGpuNum(); + if (gpuNum > remainingGpuNum){ + return LackOfResourcesEnum.LACK_OF_GPU; + }else { + return LackOfResourcesEnum.ADEQUATE; + } + } + + /** + * 添加污点 + * + * @param nodeName 节点名称 + * @param bizTaintList 污点 + * @return BizNode + */ + @Override + public BizNode taint(String nodeName, List bizTaintList) { + try { + if (StringUtils.isEmpty(nodeName) || org.springframework.util.CollectionUtils.isEmpty(bizTaintList)){ + return new BizNode().errorBadRequest(); + } + Node nodeInfo = client.nodes().withName(nodeName).get(); + if (nodeInfo == null){ + return new BizNode().error(K8sResponseEnum.NOT_FOUND.getCode(), "节点["+nodeName+"]不存在"); + } + List oldTaints = nodeInfo.getSpec().getTaints(); + for (Taint taint : oldTaints){ + if (K8sLabelConstants.PLATFORM_TAG_ISOLATION_KEY.equals(taint.getKey())){ + return new BizNode().error(K8sResponseEnum.EXISTS.getCode(),"节点已被占用"); + } + } + + Node node = client.nodes().withName(nodeName).edit().editSpec().addAllToTaints(BizConvertUtils.toTaints(bizTaintList)).endSpec().done(); + return BizConvertUtils.toBizNode(node); + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.schedulable error:{}", e); + return new BizNode().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除污点 + * + * @param nodeName 节点名称 + * @param bizTaintList 污点 + * @return BizNode + */ + @Override + public BizNode delTaint(String nodeName, List bizTaintList) { + try { + if (StringUtils.isEmpty(nodeName) || org.springframework.util.CollectionUtils.isEmpty(bizTaintList)){ + return new BizNode().errorBadRequest(); + } + Node nodeInfo = client.nodes().withName(nodeName).get(); + if (nodeInfo == null){ + return new BizNode().error(K8sResponseEnum.NOT_FOUND.getCode(), "节点["+nodeName+"]不存在"); + } + Node node = client.nodes().withName(nodeName).edit().editSpec().removeAllFromTaints(BizConvertUtils.toTaints(bizTaintList)).endSpec().done(); + return BizConvertUtils.toBizNode(node); + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.delTaint error:{}", e); + return new BizNode().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 删除污点 + * + * @param nodeName 节点名称 + * @return BizNode + */ + @Override + public BizNode delTaint(String nodeName) { + try { + if (StringUtils.isEmpty(nodeName)){ + return new BizNode().errorBadRequest(); + } + Node nodeInfo = client.nodes().withName(nodeName).get(); + if (nodeInfo == null){ + return new BizNode().error(K8sResponseEnum.NOT_FOUND.getCode(), "节点["+nodeName+"]不存在"); + } + + List taints = nodeInfo.getSpec().getTaints(); + Taint taint = new Taint(); + for (Taint obj : taints){ + if (K8sLabelConstants.PLATFORM_TAG_ISOLATION_KEY.equals(obj.getKey())){ + taint = obj; + } + } + + Node node = client.nodes().withName(nodeName) + .edit() + .editSpec() + .removeFromTaints(taint) + .endSpec() + .done(); + return BizConvertUtils.toBizNode(node); + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "NodeApiImpl.delTaint error:{}", e); + return new BizNode().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + + /** + * 根据id获取 node资源隔离 标志 + * + * @param isolationId + * @return node资源隔离 标志 + */ + @Override + public String getNodeIsolationValue(Long isolationId){ + return StrUtil.format(K8sLabelConstants.PLATFORM_TAG_ISOLATION_VALUE, SpringContextHolder.getActiveProfile(),isolationId); + } + + /** + * 获取当前用户 + * + * @return node资源隔离 标志 + */ + @Override + public String getNodeIsolationValue() { + return StrUtil.format(K8sLabelConstants.PLATFORM_TAG_ISOLATION_VALUE, SpringContextHolder.getActiveProfile(),userContextService.getCurUserId()); + } + + /** + * 获取当前用户资源隔离 Toleration + * + * @return Toleration + */ + @Override + public Toleration getNodeIsolationToleration() { + List nodes = getWithLabel(K8sLabelConstants.PLATFORM_TAG_ISOLATION_KEY,getNodeIsolationValue()); + if (CollectionUtils.isEmpty(nodes)){ + return null; + } + return ResourceBuildUtils.buildNoScheduleEqualToleration(K8sLabelConstants.PLATFORM_TAG_ISOLATION_KEY,getNodeIsolationValue()); + } + + /** + * 获取当前用户 资源隔离 NodeSelector + * @return Map + */ + @Override + public Map getNodeIsolationNodeSelector() { + Map nodeSelector = new HashMap<>(MagicNumConstant.TWO); + nodeSelector.put(K8sLabelConstants.PLATFORM_TAG_ISOLATION_KEY,getNodeIsolationValue()); + return nodeSelector; + } + + /** + * 根据userid 生成 BizTaint 列表 + * + * @param userId + * @return + */ + @Override + public List geBizTaintListByUserId(Long userId) { + List bizTaintList = new ArrayList<>(); + BizTaint bizTaint = new BizTaint(); + bizTaint.setEffect(K8sTolerationEffectEnum.NOSCHEDULE.getEffect()); + bizTaint.setKey(K8sLabelConstants.PLATFORM_TAG_ISOLATION_KEY); + bizTaint.setValue(getNodeIsolationValue(userId)); + bizTaintList.add(bizTaint); + return bizTaintList; + } + + /** + * 根据当前用户 生成 BizTaint 列表 + * + * @return + */ + @Override + public List geBizTaintListByUserId() { + return geBizTaintListByUserId(userContextService.getCurUserId()); + } + + + /** + * 查询节点内存资源是否可分配 + * + * @param memNum 单位为Mi 1Mi等于1024Ki + * @param nodeItems Node集合 + * @return List Node集合 + */ + private List isMemAllocatable(int memNum, List nodeItems) { + List nodeItemResults = new ArrayList<>(); + List nodeNameList = new ArrayList<>(); + List nodeMetrics = metricsApi.getNodeMetrics(); + + nodeItems.forEach(nodeItem -> { + String nodeName = nodeItem.getMetadata().getName(); + List collect = nodeMetrics.stream().filter(nodeMetric -> nodeMetric.getNodeName().equals(nodeName)).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(collect)){ + String memAmount = collect.get(0).getMemoryUsageAmount(); + int memCapacity = Integer.parseInt(nodeItem.getStatus().getCapacity().get(K8sParamConstants.QUANTITY_MEMORY_KEY).getAmount()) / MagicNumConstant.ONE_THOUSAND; + int memAmountInt = Integer.parseInt(memAmount) / MagicNumConstant.BINARY_TEN_EXP; + if (memCapacity - memAmountInt > memNum) { + nodeNameList.add(nodeName); + } + } + + }); + + nodeNameList.forEach(nodeName -> { + List collect = nodeItems.stream().filter(nodeItem -> nodeItem.getMetadata().getName().equals(nodeName)).collect(Collectors.toList()); + nodeItemResults.addAll(collect); + }); + + return nodeItemResults; + } + + + /** + * 查询节点Cpu资源是否可分配 + * + * @param cpuNum 单位为m 1核等于1000m + * @param nodeItems Node集合 + * @return List Node集合 + */ + private List isCpuAllocatable(int cpuNum, List nodeItems) { + + List nodeItemResults = new ArrayList<>(); + List nodeNameList = new ArrayList<>(); + List nodeMetrics = metricsApi.getNodeMetrics(); + + nodeItems.forEach(nodeItem -> { + String nodeName = nodeItem.getMetadata().getName(); + List collect = nodeMetrics.stream().filter(nodeMetric -> nodeMetric.getNodeName().equals(nodeName)).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(collect)){ + String cpuAmount = collect.get(0).getCpuUsageAmount(); + int cpuCapacity = Integer.parseInt(nodeItem.getStatus().getCapacity().get(K8sParamConstants.QUANTITY_CPU_KEY).getAmount()) * MagicNumConstant.ONE_THOUSAND; + int cpuAmountInt = (int) (Long.parseLong(cpuAmount) / MagicNumConstant.ONE_THOUSAND / MagicNumConstant.ONE_THOUSAND); + if (cpuCapacity - cpuAmountInt >= cpuNum) { + nodeNameList.add(nodeName); + } + } + }); + + nodeNameList.forEach(nodeName -> { + List collect = nodeItems.stream().filter(nodeItem -> nodeItem.getMetadata().getName().equals(nodeName)).collect(Collectors.toList()); + nodeItemResults.addAll(collect); + }); + + return nodeItemResults; + } + + + /** + * 查询节点Gpu资源是否可分配 + * + * @param gpuNum 单位为显卡,即"1"表示1张显卡 + * @param nodeItems Node集合 + * @return List Node集合 + */ + private List isGpuAllocatable(int gpuNum, List nodeItems) { + List nodeItemResults = new ArrayList<>(); + List nodeNameList = new ArrayList<>(); + nodeItems = nodeItems.stream().filter(node -> node.getStatus().getCapacity().containsKey(K8sParamConstants.GPU_RESOURCE_KEY)).collect(Collectors.toList()); + + List podItems = filterRequestGpuPod(); + Map allocatableGpu = new HashMap(); + + for (Node nodeItem : nodeItems) { + int totalGpuAmount = 0; + int totalGpu = Integer.parseInt(nodeItem.getStatus().getCapacity().get(K8sParamConstants.GPU_RESOURCE_KEY).getAmount()); + String nodeName = nodeItem.getMetadata().getName(); + List nodePodItems = podItems.stream().filter(pod -> pod.getSpec().getNodeName().equals(nodeName)).collect(Collectors.toList()); + for (Pod pod : nodePodItems) { + String gpuAmount = pod.getSpec().getContainers().get(0).getResources().getLimits().get(K8sParamConstants.GPU_RESOURCE_KEY).getAmount(); + totalGpuAmount = totalGpuAmount + Integer.parseInt(gpuAmount); + } + allocatableGpu.put(nodeName, totalGpu - totalGpuAmount); + + } + Set keySet = allocatableGpu.keySet(); + + keySet.forEach(key -> { + if (allocatableGpu.get(key) >= gpuNum) { + nodeNameList.add(key); + } + }); + + for (String nodeName : nodeNameList) { + List collect = nodeItems.stream().filter(nodeItem -> nodeItem.getMetadata().getName().equals(nodeName)).collect(Collectors.toList()); + nodeItemResults.addAll(collect); + } + return nodeItemResults; + } + + /** + * 获取申请了gpu的pod列表 + * @return + */ + private List filterRequestGpuPod(){ + PodList podList = client.pods().list(); + if (CollectionUtil.isNotEmpty(podList.getItems())){ + return podList.getItems().stream().filter(pod -> + pod.getSpec().getContainers().get(0).getResources().getLimits() != null && + pod.getSpec().getContainers().get(0).getResources().getLimits().containsKey(K8sParamConstants.GPU_RESOURCE_KEY) && + pod.getStatus().getPhase().equals(PodPhaseEnum.RUNNING.getPhase())).collect(Collectors.toList()); + } + return new ArrayList<>(); + } + + /** + * 查询集群已分配gpu数量 + * @return + */ + private Integer getAllocatedGpuNum(){ + return filterRequestGpuPod().stream().mapToInt(pod-> + pod.getSpec().getContainers().stream().mapToInt(container -> + Integer.valueOf(String.valueOf(container.getResources().getLimits().get(K8sParamConstants.GPU_RESOURCE_KEY).getAmount()))).sum()) + .sum(); + } + + /** + * 查询集群总gpu数量 + * @return + */ + private Integer getTotalGpuNum(){ + return listAll().stream() + .filter(node -> !node.isUnschedulable() && node.getCapacity().containsKey(K8sParamConstants.GPU_RESOURCE_KEY) && CollectionUtils.isEmpty(node.getTaints())) + .mapToInt(node -> Integer.valueOf(String.valueOf(node.getCapacity().get(K8sParamConstants.GPU_RESOURCE_KEY).getAmount()))).sum(); + } + + /** + * 查询节点是否存在指定的污点 + * @return + */ + private boolean doesTaintExit(Node node, BizTaint bizTaint){ + List taints = node.getSpec().getTaints().stream().filter(taint -> + StringUtils.equalsAny(taint.getKey(), bizTaint.getKey()) + && StringUtils.equalsAny(taint.getValue(), bizTaint.getValue())) + .collect(Collectors.toList()); + return CollectionUtil.isNotEmpty(taints); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PersistentVolumeClaimApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PersistentVolumeClaimApiImpl.java new file mode 100644 index 0000000..f1046e4 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PersistentVolumeClaimApiImpl.java @@ -0,0 +1,369 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.storage.StorageClass; +import io.fabric8.kubernetes.api.model.storage.StorageClassBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.PersistentVolumeClaimApi; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtPersistentVolumeClaimBO; +import org.dubhe.k8s.domain.resource.BizPersistentVolumeClaim; +import org.dubhe.k8s.enums.AccessModeEnum; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.enums.PvReclaimPolicyEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.dubhe.k8s.utils.YamlUtils; +import org.springframework.beans.factory.annotation.Value; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @description pvc api + * @date 2020-04-23 + */ +public class PersistentVolumeClaimApiImpl implements PersistentVolumeClaimApi { + private K8sUtils k8sUtils; + private KubernetesClient client; + + private final static String STORAGE_CLASS_API_VERSION = "storage.k8s.io/v1"; + private final static String PV = "pv"; + private final static String PV_SUFFIX = "-pv"; + private final static String STORAGE = "storage"; + private final static String STORAGE_CLASS_SUFFIX = "-storageclass"; + private final static String HOSTPATH_RECYCLING_STRATEGY = "DirectoryOrCreate"; + private final static String PROVISIONER = "k8s.io/minikube-hostpath"; + + @Value("${k8s.nfs-storage-class-name}") + private String nfsStorageClassName; + + public PersistentVolumeClaimApiImpl(K8sUtils k8sUtils) { + this.k8sUtils = k8sUtils; + this.client = k8sUtils.getClient(); + } + + /** + * 创建PVC + * + * @param bo PVC BO + * @return BizPersistentVolumeClaim PVC业务类 + */ + @Override + public BizPersistentVolumeClaim create(PtPersistentVolumeClaimBO bo) { + if (bo == null || StringUtils.isEmpty(bo.getNamespace()) || StringUtils.isEmpty(bo.getPvcName()) || StringUtils.isEmpty(bo.getRequest())) { + return new BizPersistentVolumeClaim().errorBadRequest(); + } + try { + Map storageClassLabels = LabelUtils.getChildLabels(bo.getResourceName(), bo.getPvcName(), K8sKindEnum.PERSISTENTVOLUMECLAIM.getKind()); + + Map pvcLabels = LabelUtils.getChildLabels(bo.getResourceName(), bo.getNamespace(), K8sKindEnum.NAMESPACE.getKind()); + + //创建StorageClass + ObjectMeta metadata = new ObjectMeta(); + metadata.setName(getStorageClassName(bo.getPvcName())); + metadata.setNamespace(bo.getNamespace()); + metadata.setLabels(storageClassLabels); + StorageClass storageClass = new StorageClassBuilder().withApiVersion(STORAGE_CLASS_API_VERSION) + .withKind(K8sKindEnum.STORAGECLASS.getKind()) + .withMetadata(metadata) + .withParameters(bo.getParameters()) + .withProvisioner(bo.getProvisioner()==null?PROVISIONER:bo.getProvisioner()) + .build(); + storageClass = client.storage().storageClasses().inNamespace(bo.getNamespace()).createOrReplace(storageClass); + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(storageClass)); + //创建pvc + PersistentVolumeClaim pvc = new PersistentVolumeClaimBuilder() + .withNewMetadata().withName(bo.getPvcName()).addToLabels(pvcLabels).addToLabels(bo.getLabels()).endMetadata() + .withNewSpec().addAllToAccessModes(bo.getAccessModes()) + .withNewResources().addToRequests(STORAGE, new Quantity(bo.getRequest())).endResources() + .withNewStorageClassName(getStorageClassName(bo.getPvcName())).endSpec().build(); + if (StringUtils.isNotEmpty(bo.getLimit())) { + pvc.getSpec().getResources().setLimits(new HashMap() {{ + put(STORAGE, new Quantity(bo.getLimit())); + }}); + } + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(pvc)); + return BizConvertUtils.toBizPersistentVolumeClaim(client.persistentVolumeClaims().inNamespace(bo.getNamespace()).create(pvc)); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PersistentVolumeClaimApi.create error, param:{} error:{}", bo, e); + return new BizPersistentVolumeClaim().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 创建挂载文件存储服务 PV + * + * @param bo PVC bo + * @return BizPersistentVolumeClaim PVC业务类 + */ + @Override + public BizPersistentVolumeClaim createWithFsPv(PtPersistentVolumeClaimBO bo) { + if (bo == null || StringUtils.isEmpty(bo.getNamespace()) || StringUtils.isEmpty(bo.getPvcName()) || StringUtils.isEmpty(bo.getRequest())) { + return new BizPersistentVolumeClaim().errorBadRequest(); + } + try { + Map pvLabels = LabelUtils.getChildLabels(bo.getResourceName(), bo.getPvcName(), K8sKindEnum.PERSISTENTVOLUMECLAIM.getKind()); + pvLabels.put(PV, bo.getPvcName() + PV_SUFFIX); + + if (client.persistentVolumes().withName(bo.getPvcName() + PV_SUFFIX).get() == null) { + //创建pv + PersistentVolume pv = new PersistentVolumeBuilder() + .withNewMetadata().addToLabels(pvLabels).withName(bo.getPvcName() + PV_SUFFIX).endMetadata() + .withNewSpec().addToCapacity(STORAGE, new Quantity(bo.getRequest())).addNewAccessMode(AccessModeEnum.READ_WRITE_ONCE.getType()).withNewPersistentVolumeReclaimPolicy(PvReclaimPolicyEnum.RECYCLE.getPolicy()) + .withNewHostPath().withNewPath(bo.getPath()).withType(K8sParamConstants.HOST_PATH_TYPE).endHostPath() + .endSpec() + .build(); + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(pv)); + client.persistentVolumes().createOrReplace(pv); + } + + Map pvcLabels = LabelUtils.getChildLabels(bo.getResourceName(), bo.getNamespace(), K8sKindEnum.NAMESPACE.getKind()); + + PersistentVolumeClaim old = client.persistentVolumeClaims().inNamespace(bo.getNamespace()).withName(bo.getPvcName()).get(); + if (old == null) { + //创建pvc + PersistentVolumeClaim pvc = new PersistentVolumeClaimBuilder() + .withNewMetadata().withName(bo.getPvcName()).addToLabels(pvcLabels).addToLabels(bo.getLabels()).endMetadata() + .withNewSpec().addAllToAccessModes(bo.getAccessModes()) + .withNewSelector().addToMatchLabels(PV, bo.getPvcName() + PV_SUFFIX).endSelector() + .withNewResources().addToRequests(STORAGE, new Quantity(bo.getRequest())).endResources() + .endSpec().build(); + if (StringUtils.isNotEmpty(bo.getLimit())) { + pvc.getSpec().getResources().setLimits(new HashMap(MagicNumConstant.SIXTEEN) {{ + put(STORAGE, new Quantity(bo.getLimit())); + }}); + } + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(pvc)); + pvc = client.persistentVolumeClaims().inNamespace(bo.getNamespace()).createOrReplace(pvc); + return BizConvertUtils.toBizPersistentVolumeClaim(pvc); + } else { + return BizConvertUtils.toBizPersistentVolumeClaim(old); + } + + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PersistentVolumeClaimApiImpl.createWithFsPv error, param:{} error:{}", bo, e); + return new BizPersistentVolumeClaim().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 创建挂载直接存储 PV + * + * @param bo PVC BO + * @return BizPersistentVolumeClaim PVC业务类 + */ + @Override + public BizPersistentVolumeClaim createWithDirectPv(PtPersistentVolumeClaimBO bo) { + if (bo == null || StringUtils.isEmpty(bo.getNamespace()) || StringUtils.isEmpty(bo.getPvcName()) || StringUtils.isEmpty(bo.getRequest())) { + return new BizPersistentVolumeClaim().errorBadRequest(); + } + try { + Map pvLabels = LabelUtils.getChildLabels(bo.getResourceName(), bo.getPvcName(), K8sKindEnum.PERSISTENTVOLUMECLAIM.getKind()); + pvLabels.put(PV, bo.getPvcName() + PV_SUFFIX); + + if (client.persistentVolumes().withName(bo.getPvcName() + PV_SUFFIX).get() == null) { + //创建pv + PersistentVolume pv = new PersistentVolumeBuilder() + .withNewMetadata().addToLabels(pvLabels).withName(bo.getPvcName() + PV_SUFFIX).endMetadata() + .withNewSpec().addToCapacity(STORAGE, new Quantity(bo.getRequest())).addNewAccessMode(AccessModeEnum.READ_WRITE_ONCE.getType()).withNewPersistentVolumeReclaimPolicy("Recycle") + .withNewHostPath(bo.getPath(),HOSTPATH_RECYCLING_STRATEGY) + .endSpec() + .build(); + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(pv)); + client.persistentVolumes().createOrReplace(pv); + } + + Map pvcLabels = LabelUtils.getChildLabels(bo.getResourceName(), bo.getNamespace(), K8sKindEnum.NAMESPACE.getKind()); + + PersistentVolumeClaim old = client.persistentVolumeClaims().inNamespace(bo.getNamespace()).withName(bo.getPvcName()).get(); + if (old == null) { + //创建pvc + PersistentVolumeClaim pvc = new PersistentVolumeClaimBuilder() + .withNewMetadata().withName(bo.getPvcName()).addToLabels(pvcLabels).addToLabels(bo.getLabels()).endMetadata() + .withNewSpec().addAllToAccessModes(bo.getAccessModes()) + .withNewSelector().addToMatchLabels(PV, bo.getPvcName() + PV_SUFFIX).endSelector() + .withNewResources().addToRequests(STORAGE, new Quantity(bo.getRequest())).endResources() + .endSpec().build(); + if (StringUtils.isNotEmpty(bo.getLimit())) { + pvc.getSpec().getResources().setLimits(new HashMap(MagicNumConstant.SIXTEEN) {{ + put(STORAGE, new Quantity(bo.getLimit())); + }}); + } + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(pvc)); + pvc = client.persistentVolumeClaims().inNamespace(bo.getNamespace()).createOrReplace(pvc); + return BizConvertUtils.toBizPersistentVolumeClaim(pvc); + } else { + return BizConvertUtils.toBizPersistentVolumeClaim(old); + } + + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PersistentVolumeClaimApiImpl.createWithDirectPv error, param:{} error:{}", bo, e); + return new BizPersistentVolumeClaim().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 创建创建自动挂载nfs动态存储的PVC + * + * @param bo PVC bo + * @return BizPersistentVolumeClaim PVC业务类 + */ + @Override + public BizPersistentVolumeClaim createDynamicNfs(PtPersistentVolumeClaimBO bo) { + if (bo == null || StringUtils.isEmpty(bo.getNamespace()) || StringUtils.isEmpty(bo.getPvcName()) || StringUtils.isEmpty(bo.getRequest())) { + return new BizPersistentVolumeClaim().errorBadRequest(); + } + try { + Map pvcLabels = LabelUtils.getChildLabels(bo.getResourceName(), bo.getNamespace(), K8sKindEnum.NAMESPACE.getKind()); + + PersistentVolumeClaim old = client.persistentVolumeClaims().inNamespace(bo.getNamespace()).withName(bo.getPvcName()).get(); + if (old == null) { + //创建pvc + PersistentVolumeClaim pvc = new PersistentVolumeClaimBuilder() + .withNewMetadata().withName(bo.getPvcName()).addToLabels(pvcLabels).addToLabels(bo.getLabels()).endMetadata() + .withNewSpec().addAllToAccessModes(bo.getAccessModes()) + .withNewStorageClassName(nfsStorageClassName) + .withNewResources().addToRequests(STORAGE, new Quantity(bo.getRequest())).endResources() + .endSpec().build(); + if (StringUtils.isNotEmpty(bo.getLimit())) { + pvc.getSpec().getResources().setLimits(new HashMap(MagicNumConstant.SIXTEEN) {{ + put(STORAGE, new Quantity(bo.getLimit())); + }}); + } + pvc = client.persistentVolumeClaims().inNamespace(bo.getNamespace()).createOrReplace(pvc); + LogUtil.info(LogEnum.BIZ_K8S, YamlUtils.dumpAsYaml(pvc)); + return BizConvertUtils.toBizPersistentVolumeClaim(pvc); + } else { + return BizConvertUtils.toBizPersistentVolumeClaim(old); + } + + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PersistentVolumeClaimApiImpl.createDynamicNfs error, param:{} error:{}", bo, e); + return new BizPersistentVolumeClaim().error(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 查询命名空间下所有 PVC + * + * @param namespace 命名空间 + * @return List PVC业务类集合 + */ + @Override + public List list(String namespace) { + if (StringUtils.isEmpty(namespace)) { + return Collections.EMPTY_LIST; + } + PersistentVolumeClaimList persistentVolumeClaimList = client.persistentVolumeClaims().inNamespace(namespace).list(); + return persistentVolumeClaimList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizPersistentVolumeClaim(obj)).collect(Collectors.toList()); + } + + /** + * 删除具体的PVC + * + * @param namespace 命名空间 + * @param pvcName PVC名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult delete(String namespace, String pvcName) { + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(pvcName)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + client.storage().storageClasses().inNamespace(namespace).withName(getStorageClassName(pvcName)).delete(); + client.persistentVolumeClaims().inNamespace(namespace).withName(pvcName).delete(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PersistentVolumeClaimApiImpl.delete error, param:[namespace]={}, error:{}", namespace, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 回收存储(recycle 的pv才能回收) + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult recycle(String namespace, String resourceName) { + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceName)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + client.persistentVolumeClaims().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete(); + client.persistentVolumes().withLabels(LabelUtils.withEnvResourceName(resourceName)).delete(); + return new PtBaseResult(); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PersistentVolumeClaimApiImpl.recycle error, param:[namespace]={}, [resourceName]={}, error:{}",namespace, resourceName, e); + return new PtBaseResult(String.valueOf(e.getCode()), e.getMessage()); + } + } + + /** + * 拼接storageClassName + * + * @param pvcName PVC名称 + * @return String 动态PVC的名称 + */ + @Override + public String getStorageClassName(String pvcName) { + if (StringUtils.isEmpty(pvcName)) { + return null; + } else { + return pvcName + STORAGE_CLASS_SUFFIX; + } + } + + /** + * 删除PV + * + * @param pvName PV名称 + * @return boolean true成功 false失败 + */ + @Override + public boolean deletePv(String pvName) { + return client.persistentVolumes().withName(pvName).delete(); + } + + /** + * 查询PV + * + * @param pvName PV名称 + * @return PersistentVolume PV实体类 + */ + @Override + public PersistentVolume getPv(String pvName) { + return client.persistentVolumes().withName(pvName).get(); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PodApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PodApiImpl.java new file mode 100644 index 0000000..643ff26 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/PodApiImpl.java @@ -0,0 +1,446 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpStatus; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.RegexUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.JupyterResourceApi; +import org.dubhe.k8s.api.MetricsApi; +import org.dubhe.k8s.api.PodApi; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.bo.LabelBO; +import org.dubhe.k8s.domain.resource.BizPod; +import org.dubhe.k8s.domain.vo.PtJupyterDeployVO; +import org.dubhe.k8s.domain.vo.PtPodsVO; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.PodPhaseEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @description PodApi实现类 + * @date 2020-04-15 + */ +public class PodApiImpl implements PodApi { + + private static final String TOKEN_REGEX = "token=[\\d|a-z]*"; + private static final String POD_URL = "http://{}:{}?{}"; + + private K8sUtils k8sUtils; + private KubernetesClient client; + + @Autowired + private JupyterResourceApi jupyterResourceApi; + @Autowired + private MetricsApi metricsApi; + + public PodApiImpl(K8sUtils k8sUtils) { + this.k8sUtils = k8sUtils; + this.client = k8sUtils.getClient(); + } + + /** + * 根据Pod名称和命名空间查询Pod + * + * @param namespace 命名空间 + * @param podName Pod名称 + * @return BizPod Pod业务类 + */ + @Override + public BizPod get(String namespace, String podName) { + try{ + LogUtil.info(LogEnum.BIZ_K8S,"Input namespace={};podName={}", namespace,podName); + if (StringUtils.isEmpty(namespace)) { + return new BizPod().baseErrorBadRequest(); + } + Pod pod = client.pods().inNamespace(namespace).withName(podName).get(); + BizPod bizPod = BizConvertUtils.toBizPod(pod); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", bizPod); + return bizPod; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.get error, param:[namespace]={}, [podName]={}, error:{}",namespace, podName, e); + return new BizPod().error(String.valueOf(e.getCode()),e.getMessage()); + } + } + + /** + * 根据Pod名称列表和命名空间查询Pod列表 + * + * @param namespace 命名空间 + * @param podNames Pod名称 + * @return List Pod业务类列表 + */ + @Override + public List get(String namespace, List podNames) { + try{ + List bizPodList = new ArrayList<>(); + LogUtil.info(LogEnum.BIZ_K8S,"Input namespace={};podNames={}", namespace,podNames); + if (StringUtils.isEmpty(namespace)) { + return bizPodList; + } + PodList podList = client.pods().inNamespace(namespace).list(); + if (podList == null || CollectionUtils.isEmpty(podList.getItems())){ + return bizPodList; + } + for (Pod pod : podList.getItems()){ + if (podNames.contains(pod.getMetadata().getName())){ + bizPodList.add(BizConvertUtils.toBizPod(pod)); + } + } + return bizPodList; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.get error, param:[namespace]={}, [podNames]={}, error:{}",namespace, podNames, e); + return new ArrayList<>(); + } + } + + /** + * 根据命名空间和资源名查询Pod + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizPod Pod业务类 + */ + @Override + public BizPod getWithResourceName(String namespace, String resourceName) { + try { + LogUtil.info(LogEnum.BIZ_K8S,"Input namespace={};resourceName={}", namespace,resourceName); + if (StringUtils.isEmpty(namespace)) { + return new BizPod().baseErrorBadRequest(); + } + PodList podList = client.pods().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if (CollectionUtil.isEmpty(podList.getItems())) { + return new BizPod().error(K8sResponseEnum.NOT_FOUND.getCode(), K8sResponseEnum.NOT_FOUND.getMessage()); + } + Pod pod = podList.getItems().get(0); + BizPod bizPod = BizConvertUtils.toBizPod(pod); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", bizPod); + return bizPod; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.getWithResourceName error, param:[namespace]={}, [resourceName]={}, error:{}",namespace, resourceName, e); + return new BizPod().error(String.valueOf(e.getCode()),e.getMessage()); + } + } + + /** + * 根据命名空间和资源名查询Pod集合 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return List Pod业务类集合 + */ + @Override + public List getListByResourceName(String namespace, String resourceName) { + try{ + LogUtil.info(LogEnum.BIZ_K8S,"Input namespace={};resourceName={}", namespace,resourceName); + if (StringUtils.isEmpty(namespace)) { + return Collections.EMPTY_LIST; + } + PodList podList = client.pods().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if (CollectionUtil.isEmpty(podList.getItems())) { + return Collections.EMPTY_LIST; + } + List bizPodList = BizConvertUtils.toBizPodList(podList.getItems()); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", bizPodList); + return bizPodList; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.getWithResourceName error, param:[namespace]={}, [resourceName]={}, error:{}",namespace, resourceName, e); + return Collections.EMPTY_LIST; + } + } + + /** + * 查询命名空间下所有Pod + * + * @param namespace 命名空间 + * @return List Pod业务类集合 + */ + @Override + public List getWithNamespace(String namespace) { + try{ + List bizPodList = new ArrayList<>(); + PodList podList = client.pods().inNamespace(namespace).list(); + if (CollectionUtil.isEmpty(podList.getItems())) { + return bizPodList; + } + bizPodList = BizConvertUtils.toBizPodList(podList.getItems()); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", bizPodList); + return bizPodList; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.getWithNamespace error, param:[namespace]={}, error:{}",namespace, e); + return Collections.EMPTY_LIST; + } + + } + + /** + * 查询集群所有Pod + * + * @return List Pod业务类集合 + */ + @Override + public List listAll() { + try{ + List bizPodList = client.pods().inAnyNamespace().list().getItems().parallelStream().map(obj -> BizConvertUtils.toBizPod(obj)).collect(Collectors.toList()); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", bizPodList); + return bizPodList; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.listAll error:", e); + return Collections.EMPTY_LIST; + } + } + + /** + *根据dtname查询pod信息 + * + * @param dtname 自定义dt的名称 + * @return List pod业务类集合 + */ + @Override + public List findByDtName(String dtname) { + List items = client.pods().list().getItems(); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}",items); + List bizPods = new ArrayList<>(); + if (!(CollectionUtil.isEmpty(items))) { + items.stream().forEach(pod -> { + Map labels = pod.getMetadata().getLabels(); + if (labels != null) { + String dtName = labels.get("dt-name"); + if (dtName != null) { + if (dtName.equals(dtname)) { + bizPods.add(BizConvertUtils.toBizPod(pod)); + } + } + } + }); + } + return bizPods; + } + + /** + * 根据Node分组获得所有运行中的Pod + * + * @return Map> 键为Node名称,值为Pod业务类集合 + */ + @Override + public Map> listAllRuningPodGroupByNodeName() { + try{ + List bizPodList = client.pods().inAnyNamespace().list().getItems().parallelStream().map(obj -> BizConvertUtils.toBizPod(obj)).collect(Collectors.toList()); + Map> map = bizPodList.parallelStream().filter(pod -> PodPhaseEnum.RUNNING.getPhase().equals(pod.getPhase())).collect(Collectors.groupingBy(BizPod::getNodeName)); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", map); + return map; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.listAllRunningPodGroupByNodeName error:{}", e); + return Collections.EMPTY_MAP; + } + } + + + /** + * 根据Node分组获取Pod信息 + * + * @return Map> 键为Node名称,值为Pod结果类集合 + */ + @Override + public Map> getPods(){ + try{ + Map> map = metricsApi.getPodsMetricsRealTime().stream().collect(Collectors.groupingBy(PtPodsVO::getNodeName)); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", map); + return map; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.getPods error:{}", e); + return Collections.EMPTY_MAP; + } + } + + + /** + * 根据label查询Pod集合 + * + * @param labelBO k8s label资源 bo + * @return List Pod 实体类集合 + */ + @Override + public List list(LabelBO labelBO) { + return client.pods().inAnyNamespace().withLabels(LabelUtils.withEnvLabel(labelBO.getKey(), labelBO.getValue())).list().getItems(); + } + + /** + * 根据多个label查询Pod集合 + * + * @param labelBos label资源 bo 的集合 + * @return List Pod 实体类集合 + */ + @Override + public List list(Set labelBos) { + Map labelMap = labelBos.stream().collect(Collectors.toMap(LabelBO::getKey, LabelBO::getValue)); + return client.pods().inAnyNamespace().withLabels(labelMap).list().getItems(); + } + + /** + * 根据命名空间查询Pod集合 + * + * @param namespace 命名空间 + * @return List Pod 实体类集合 + */ + @Override + public List list(String namespace) { + return client.pods().inNamespace(namespace).list().getItems(); + } + + /** + * 根据命名空间和label查询Pod集合 + * + * @param namespace 命名空间 + * @param labelBO label资源 bo + * @return List Pod 实体类集合 + */ + @Override + public List list(String namespace, LabelBO labelBO) { + return client.pods().inNamespace(namespace).withLabels(LabelUtils.withEnvLabel(labelBO.getKey(), labelBO.getValue())).list().getItems(); + } + + /** + * 根据命名空间和多个label查询Pod集合 + * + * @param namespace 命名空间 + * @param labelBos label资源 bo 的集合 + * @return List Pod 实体类集合 + */ + @Override + public List list(String namespace, Set labelBos) { + Map labelMap = labelBos.stream().collect(Collectors.toMap(LabelBO::getKey, LabelBO::getValue)); + return client.pods().inNamespace(namespace).withLabels(labelMap).list().getItems(); + } + + + + /** + * 根据命名空间和Pod名称查询Token信息 + * + * @param namespace 命名空间 + * @param podName Pod名称 + * @return String token + */ + @Override + public String getToken(String namespace, String podName) { + try { + String podLog = client.pods().inNamespace(namespace).withName(podName).getLog(); + return RegexUtil.getMatcher(podLog, TOKEN_REGEX); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.getToken error params:[namespace]={}, [podName]={}, error:{}",namespace, podName, e); + } + return ""; + } + + /** + * 根据命名空间和资源名获得Token信息 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return String token + */ + @Override + public String getTokenByResourceName(String namespace, String resourceName) { + try { + PodList podList = client.pods().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if (podList != null && CollectionUtil.isNotEmpty(podList.getItems())) { + String podLog = client.pods().inNamespace(namespace).withName(podList.getItems().get(0).getMetadata().getName()).getLog(); + return RegexUtil.getMatcher(podLog, TOKEN_REGEX); + } + return ""; + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_K8S, "PodApiImpl.getTokenByResourceName error, params:[namespace]={}, [resourceName]={}, error:{}",namespace, resourceName, e); + } + return ""; + } + + /** + * 根据命名空间和资源名查询Notebook url + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return String validateJupyterUrl + */ + @Override + public String getUrlByResourceName(String namespace, String resourceName) { + LogUtil.info(LogEnum.BIZ_K8S,"Start GetUrlByResourceName {} {}",namespace,resourceName); + PtJupyterDeployVO info = jupyterResourceApi.get(namespace, resourceName); + if (info != null && info.getIngressInfo() != null && CollectionUtil.isNotEmpty(info.getIngressInfo().getRules())) { + String token = getTokenByResourceName(namespace, resourceName); + if (StringUtils.isBlank(token)) { + LogUtil.info(LogEnum.BIZ_K8S, "GetUrlByResourceName Jupyter Notebook token not generated,[namespace]={}, [resourceName]={}", namespace, resourceName); + return ""; + } + String url = StrUtil.format(POD_URL, info.getIngressInfo().getRules().get(0).getHost(), k8sUtils.getPort(), token); + return validateJupyterUrl(url); + } + LogUtil.info(LogEnum.BIZ_K8S, "GetUrlByResourceName Jupyter statefulset not created,[namespace]={}, [resourceName]={}",namespace,resourceName); + return ""; + } + + /** + * 验证访问Notebook的url + * + * @param jupyterUrl 访问Notebook的url + * @return String jupyterUrl jupyter路径 + */ + private String validateJupyterUrl(String jupyterUrl) { + if (StringUtils.isBlank(jupyterUrl) || !jupyterUrl.contains(SymbolConstant.QUESTION+ K8sParamConstants.TOKEN)){ + return ""; + } + try { + HttpRequest httpRequest = HttpRequest.get(jupyterUrl); + HttpResponse httpResponse = httpRequest.execute(); + if (httpResponse == null){ + LogUtil.info(LogEnum.BIZ_K8S, "ValidateJupyterUrl failed URL[{}] HttpResponse is null",jupyterUrl); + return ""; + } + int status = httpResponse.getStatus(); + if (HttpStatus.HTTP_OK != status && HttpStatus.HTTP_MOVED_TEMP != status){ + LogUtil.info(LogEnum.BIZ_K8S, "ValidateJupyterUrl failed URL[{}] status[{}]",jupyterUrl,status); + }else { + LogUtil.info(LogEnum.BIZ_K8S, "ValidateJupyterUrl success URL[{}] status[{}]",jupyterUrl,status); + return jupyterUrl; + } + }catch (IORuntimeException e){ + LogUtil.info(LogEnum.BIZ_K8S, "ValidateJupyterUrl failed URL[{}], error message[{}]",jupyterUrl, e.getMessage()); + } + return ""; + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceIisolationApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceIisolationApiImpl.java new file mode 100644 index 0000000..280729d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceIisolationApiImpl.java @@ -0,0 +1,124 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import io.fabric8.kubernetes.api.model.Toleration; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.api.model.batch.Job; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.api.ResourceIisolationApi; +import org.dubhe.k8s.domain.cr.DistributeTrain; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Arrays; + +/** + * @description + * @date 2021-05-20 + */ +@Service +public class ResourceIisolationApiImpl implements ResourceIisolationApi { + @Autowired + private NodeApi nodeApi; + + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param job + */ + @Override + public void addIisolationInfo(Job job) { + Toleration toleration = nodeApi.getNodeIsolationToleration(); + if (toleration == null){ + return; + } + if (null == job.getSpec().getTemplate().getSpec().getNodeSelector()){ + job.getSpec().getTemplate().getSpec().setNodeSelector(nodeApi.getNodeIsolationNodeSelector()); + }else { + job.getSpec().getTemplate().getSpec().getNodeSelector().putAll(nodeApi.getNodeIsolationNodeSelector()); + } + if (null == job.getSpec().getTemplate().getSpec().getTolerations()){ + job.getSpec().getTemplate().getSpec().setTolerations(Arrays.asList(toleration)); + }else { + job.getSpec().getTemplate().getSpec().getTolerations().addAll(Arrays.asList(toleration)); + } + } + + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param deployment + */ + @Override + public void addIisolationInfo(Deployment deployment) { + Toleration toleration = nodeApi.getNodeIsolationToleration(); + if (toleration == null){ + return; + } + if (null == deployment.getSpec().getTemplate().getSpec().getNodeSelector()){ + deployment.getSpec().getTemplate().getSpec().setNodeSelector(nodeApi.getNodeIsolationNodeSelector()); + }else { + deployment.getSpec().getTemplate().getSpec().getNodeSelector().putAll(nodeApi.getNodeIsolationNodeSelector()); + } + if (null == deployment.getSpec().getTemplate().getSpec().getTolerations()){ + deployment.getSpec().getTemplate().getSpec().setTolerations(Arrays.asList(toleration)); + }else { + deployment.getSpec().getTemplate().getSpec().getTolerations().addAll(Arrays.asList(toleration)); + } + } + + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param statefulSet + */ + @Override + public void addIisolationInfo(StatefulSet statefulSet) { + Toleration toleration = nodeApi.getNodeIsolationToleration(); + if (toleration == null){ + return; + } + if (null == statefulSet.getSpec().getTemplate().getSpec().getNodeSelector()){ + statefulSet.getSpec().getTemplate().getSpec().setNodeSelector(nodeApi.getNodeIsolationNodeSelector()); + }else { + statefulSet.getSpec().getTemplate().getSpec().getNodeSelector().putAll(nodeApi.getNodeIsolationNodeSelector()); + } + if (null == statefulSet.getSpec().getTemplate().getSpec().getTolerations()){ + statefulSet.getSpec().getTemplate().getSpec().setTolerations(Arrays.asList(toleration)); + }else { + statefulSet.getSpec().getTemplate().getSpec().getTolerations().addAll(Arrays.asList(toleration)); + } + } + + /** + * 添加 node资源隔离信息 Toleration 和 node Selector + * + * @param distributeTrain + */ + @Override + public void addIisolationInfo(DistributeTrain distributeTrain) { + Toleration toleration = nodeApi.getNodeIsolationToleration(); + if (toleration == null){ + return; + } + distributeTrain.getSpec().addNodeSelector(nodeApi.getNodeIsolationNodeSelector()); + distributeTrain.getSpec().addTolerations(Arrays.asList(toleration)); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceQuotaApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceQuotaApiImpl.java new file mode 100644 index 0000000..4d6289c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/ResourceQuotaApiImpl.java @@ -0,0 +1,236 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.ResourceQuota; +import io.fabric8.kubernetes.api.model.ResourceQuotaBuilder; +import io.fabric8.kubernetes.api.model.ResourceQuotaList; +import io.fabric8.kubernetes.api.model.ScopeSelector; +import io.fabric8.kubernetes.api.model.ScopeSelectorBuilder; +import io.fabric8.kubernetes.api.model.ScopedResourceSelectorRequirement; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.bo.PtResourceQuotaBO; +import org.dubhe.k8s.domain.resource.BizQuantity; +import org.dubhe.k8s.domain.resource.BizResourceQuota; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.UnitConvertUtils; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @description 资源配额 接口实现 + * @date 2020-04-23 + */ +public class ResourceQuotaApiImpl implements ResourceQuotaApi { + private KubernetesClient client; + + public ResourceQuotaApiImpl(K8sUtils k8sUtils) { + this.client = k8sUtils.getClient(); + } + + /** + * 创建 ResourceQuota + * + * @param bo ResourceQuota BO + * @return BizResourceQuota ResourceQuota 业务类 + */ + @Override + public BizResourceQuota create(PtResourceQuotaBO bo) { + try { + LogUtil.info(LogEnum.BIZ_K8S,"Input bo={}", bo); + Gson gson = new Gson(); + List scopeSelector = gson.fromJson(gson.toJson(bo.getScopeSelector()), new TypeToken>() { + }.getType()); + Map hard = new HashMap<>(); + for (Map.Entry obj : bo.getHard().entrySet()) { + hard.put(obj.getKey(), new Quantity(obj.getValue().getAmount(), obj.getValue().getFormat())); + } + ResourceQuota resourceQuota = null; + if (scopeSelector != null){ + ScopeSelector item = new ScopeSelectorBuilder().addAllToMatchExpressions(scopeSelector).build(); + resourceQuota = new ResourceQuotaBuilder().withNewMetadata().withName(bo.getName()).endMetadata() + .withNewSpec().withHard(hard).withNewScopeSelectorLike(item).endScopeSelector().endSpec().build(); + + }else { + resourceQuota = new ResourceQuotaBuilder().withNewMetadata().withName(bo.getName()).endMetadata() + .withNewSpec().withHard(hard).endSpec().build(); + } + BizResourceQuota bizResourceQuota = BizConvertUtils.toBizResourceQuota(client.resourceQuotas().inNamespace(bo.getNamespace()).create(resourceQuota)); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", bizResourceQuota); + return bizResourceQuota; + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ResourceQuotaApiImpl.create error, param:{} error:{}", bo, e); + return new BizResourceQuota().error(String.valueOf(e.getCode()),e.getMessage()); + } + } + + /** + * 创建 ResourceQuota + * @param namespace 命名空间 + * @param name ResourceQuota 名称 + * @param cpu cpu限制 单位核 0表示不限制 + * @param memory 内存限制 单位Gi 0表示不限制 + * @param gpu gpu限制 单位张 0表示不限制 + * @return + */ + @Override + public BizResourceQuota create(String namespace, String name, Integer cpu, Integer memory, Integer gpu) { + try { + LogUtil.info(LogEnum.BIZ_K8S,"Input namespace={},name={},cpu={},mem={},gpu={}", namespace,name,cpu,memory,gpu); + if (StringUtils.isEmpty(namespace)){ + return new BizResourceQuota().error(K8sResponseEnum.BAD_REQUEST.getCode(), "namespace is empty"); + } + if (cpu == null && memory == null && gpu == null){ + return new BizResourceQuota().error(K8sResponseEnum.BAD_REQUEST.getCode(), "cpu mem gpu is empty"); + } + PtResourceQuotaBO bo = new PtResourceQuotaBO(); + bo.setNamespace(namespace); + bo.setName(StringUtils.isEmpty(name)?namespace:namespace); + if (cpu != null && cpu > 0){ + bo.addCpuLimitsHard(String.valueOf(cpu), SymbolConstant.BLANK); + } + if (memory > 0){ + bo.addMemoryLimitsHard(String.valueOf(memory), K8sParamConstants.MEM_UNIT_GI); + } + if (gpu > 0){ + bo.addGpuLimitsHard(String.valueOf(gpu)); + } + return create(bo); + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ResourceQuotaApiImpl.create error, param:{} error:{}", e); + return new BizResourceQuota().error(String.valueOf(e.getCode()),e.getMessage()); + } + } + + /** + * 根据命名空间查询ResourceQuota集合 + * + * @param namespace 命名空间 + * @return List ResourceQuota 业务类集合 + */ + @Override + public List list(String namespace) { + try { + LogUtil.info(LogEnum.BIZ_K8S,"Input namespace={}", namespace); + if (StringUtils.isEmpty(namespace)) { + ResourceQuotaList resourceQuotaList = client.resourceQuotas().inAnyNamespace().list(); + return resourceQuotaList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizResourceQuota(obj)).collect(Collectors.toList()); + } else { + ResourceQuotaList resourceQuotaList = client.resourceQuotas().inNamespace(namespace).list(); + List bizResourceQuotaList = resourceQuotaList.getItems().parallelStream().map(obj -> BizConvertUtils.toBizResourceQuota(obj)).collect(Collectors.toList()); + LogUtil.info(LogEnum.BIZ_K8S,"Output {}", bizResourceQuotaList); + return bizResourceQuotaList; + } + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ResourceQuotaApiImpl.list error, param:[namespace]={},error:{}", namespace,e); + return Collections.EMPTY_LIST; + } + } + + /** + * 删除ResourceQuota + * + * @param namespace 命名空间 + * @param name ResourceQuota 名称 + * @return PtBaseResult 基础结果类 + */ + @Override + public PtBaseResult delete(String namespace, String name) { + LogUtil.info(LogEnum.BIZ_K8S,"Input namespace={};name={}", namespace,name); + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(name)) { + return new PtBaseResult().baseErrorBadRequest(); + } + try { + if (client.resourceQuotas().inNamespace(namespace).withName(name).delete()){ + return new PtBaseResult(); + }else { + return K8sResponseEnum.REPEAT.toPtBaseResult(); + } + } catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ResourceQuotaApiImpl.delete error, param:[namespace]={}, [name]={}, error:{}",namespace, name, e); + return new PtBaseResult(String.valueOf(e.getCode()),e.getMessage()); + } + } + + /** + * 判断资源是否达到限制 + * + * @param cpuNum 单位为m 1核等于1000m + * @param memNum 单位为Mi 1Mi等于1024Ki + * @param gpuNum 单位为显卡,即"1"表示1张显卡 + * @return LimitsOfResourcesEnum 资源超限枚举类 + */ + @Override + public LimitsOfResourcesEnum reachLimitsOfResources(String namespace,Integer cpuNum, Integer memNum, Integer gpuNum) { + if (StringUtils.isEmpty(namespace)){ + return LimitsOfResourcesEnum.ADEQUATE; + } + List bizResourceQuotas = list(namespace); + if (CollectionUtils.isEmpty(bizResourceQuotas)){ + return LimitsOfResourcesEnum.ADEQUATE; + } + for (BizResourceQuota bizResourceQuota : bizResourceQuotas){ + if (!CollectionUtils.isEmpty(bizResourceQuota.getMatchExpressions())){ + continue; + } + Map remainder = bizResourceQuota.getRemainder(); + BizQuantity cpuRemainder = remainder.get(K8sParamConstants.RESOURCE_QUOTA_CPU_LIMITS_KEY); + if (cpuRemainder != null && cpuNum != null){ + if (UnitConvertUtils.cpuFormatToN(cpuRemainder.getAmount(),cpuRemainder.getFormat()) < cpuNum * MagicNumConstant.MILLION_LONG){ + return LimitsOfResourcesEnum.LIMITS_OF_CPU; + } + } + + BizQuantity memRemainder = remainder.get(K8sParamConstants.RESOURCE_QUOTA_MEMORY_LIMITS_KEY); + if (memRemainder != null && memRemainder != null){ + if (UnitConvertUtils.memFormatToMi(memRemainder.getAmount(),memRemainder.getFormat()) < memNum){ + return LimitsOfResourcesEnum.LIMITS_OF_MEM; + } + } + + BizQuantity gpuRemainder = remainder.get(K8sParamConstants.RESOURCE_QUOTA_GPU_LIMITS_KEY); + if (gpuRemainder != null && gpuNum != null){ + if (Integer.valueOf(gpuRemainder.getAmount()) < gpuNum){ + return LimitsOfResourcesEnum.LIMITS_OF_GPU; + } + } + } + + return LimitsOfResourcesEnum.ADEQUATE; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/TrainJobApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/TrainJobApiImpl.java new file mode 100644 index 0000000..009f5cd --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/TrainJobApiImpl.java @@ -0,0 +1,485 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.google.common.collect.Maps; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import io.fabric8.kubernetes.api.model.batch.Job; +import io.fabric8.kubernetes.api.model.batch.JobBuilder; +import io.fabric8.kubernetes.api.model.batch.JobList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.LogMonitoringApi; +import org.dubhe.k8s.api.NodeApi; +import org.dubhe.k8s.api.PersistentVolumeClaimApi; +import org.dubhe.k8s.api.PodApi; +import org.dubhe.k8s.api.ResourceIisolationApi; +import org.dubhe.k8s.api.ResourceQuotaApi; +import org.dubhe.k8s.api.TrainJobApi; +import org.dubhe.k8s.cache.ResourceCache; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.bo.PtJupyterJobBO; +import org.dubhe.k8s.domain.bo.PtMountDirBO; +import org.dubhe.k8s.domain.bo.PtPersistentVolumeClaimBO; +import org.dubhe.k8s.domain.bo.TaskYamlBO; +import org.dubhe.k8s.domain.entity.K8sTask; +import org.dubhe.k8s.domain.resource.BizJob; +import org.dubhe.k8s.domain.resource.BizPersistentVolumeClaim; +import org.dubhe.k8s.domain.vo.PtJupyterJobVO; +import org.dubhe.k8s.enums.ImagePullPolicyEnum; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.enums.K8sResponseEnum; +import org.dubhe.k8s.enums.LackOfResourcesEnum; +import org.dubhe.k8s.enums.LimitsOfResourcesEnum; +import org.dubhe.k8s.enums.RestartPolicyEnum; +import org.dubhe.k8s.enums.ShellCommandEnum; +import org.dubhe.k8s.service.K8sTaskService; +import org.dubhe.k8s.utils.BizConvertUtils; +import org.dubhe.k8s.utils.K8sUtils; +import org.dubhe.k8s.utils.LabelUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Resource; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @description TrainJobApi实现类 + * @date 2020-04-22 + */ +public class TrainJobApiImpl implements TrainJobApi { + + private K8sUtils k8sUtils; + private KubernetesClient client; + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + @Autowired + private PersistentVolumeClaimApi persistentVolumeClaimApi; + @Autowired + private NodeApi nodeApi; + @Autowired + private PodApi podApi; + @Autowired + private LogMonitoringApi logMonitoringApi; + @Autowired + private K8sTaskService k8sTaskService; + @Autowired + private ResourceCache resourceCache; + @Autowired + private ResourceQuotaApi resourceQuotaApi; + @Autowired + private ResourceIisolationApi resourceIisolationApi; + + public TrainJobApiImpl(K8sUtils k8sUtils) { + this.k8sUtils = k8sUtils; + this.client = k8sUtils.getClient(); + } + + /** + * 创建训练任务 Job + * + * @param bo 训练任务 Job BO + * @return PtJupyterJobVO 训练任务 Job 结果类 + */ + @Override + public PtJupyterJobVO create(PtJupyterJobBO bo) { + try{ + LimitsOfResourcesEnum limitsOfResources = resourceQuotaApi.reachLimitsOfResources(bo.getNamespace(),bo.getCpuNum(), bo.getMemNum(), bo.getGpuNum()); + if (!LimitsOfResourcesEnum.ADEQUATE.equals(limitsOfResources)){ + return new PtJupyterJobVO().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(), limitsOfResources.getMessage()); + } + LackOfResourcesEnum lack = nodeApi.isAllocatable(bo.getCpuNum(),bo.getMemNum(),bo.getGpuNum()); + if (!LackOfResourcesEnum.ADEQUATE.equals(lack)){ + return new PtJupyterJobVO().error(K8sResponseEnum.LACK_OF_RESOURCES.getCode(),lack.getMessage()); + } + LogUtil.info(LogEnum.BIZ_K8S, "Params of creating Job--create:{}", bo); + if (!fileStoreApi.createDirs(bo.getDirList().toArray(new String[MagicNumConstant.ZERO]))) { + return new PtJupyterJobVO().error(K8sResponseEnum.INTERNAL_SERVER_ERROR.getCode(), K8sResponseEnum.INTERNAL_SERVER_ERROR.getMessage()); + } + resourceCache.deletePodCacheByResourceName(bo.getNamespace(),bo.getName()); + PtJupyterJobVO result = new JupyterDeployer(bo).buildVolumes().deploy(); + LogUtil.info(LogEnum.BIZ_K8S,"Return value of creating Job--create:{}", result); + return result; + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S,"TrainJobApiImpl.create error, param:{} error:{}", bo, e); + return new PtJupyterJobVO().error(String.valueOf(e.getCode()),e.getMessage()); + } + } + + /** + * 根据命名空间和资源名删除Job + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return Boolean true成功 false失败 + */ + @Override + public Boolean delete(String namespace, String resourceName){ + try { + LogUtil.info(LogEnum.BIZ_K8S,"Params of delete Job--namespace:{}, resourceName:{}",namespace, resourceName); + k8sTaskService.deleteByNamespaceAndResourceName(namespace,resourceName); + JobList jobList = client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if (jobList == null || jobList.getItems().size() == 0){ + return true; + } + return client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).delete(); + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "TrainJobApiImpl.delete error, param:[namespace]={}, [resourceName]={}, error:{}",namespace, resourceName,e); + return false; + } + } + + /** + * 根据命名空间查询Job + * + * @param namespace 命名空间 + * @return List Job业务类集合 + */ + @Override + public List list(String namespace){ + JobList list = client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName()).list(); + if(CollectionUtil.isEmpty(list.getItems())){ + return null; + } + return list.getItems().stream().map(item -> BizConvertUtils.toBizJob(item)).collect(Collectors.toList()); + } + + /** + * 根据命名空间和资源名查询Job + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return BizJob Job业务类 + */ + @Override + public BizJob get(String namespace, String resourceName){ + try { + JobList list = client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(resourceName)).list(); + if(CollectionUtil.isEmpty(list.getItems())){ + return null; + } + Job job = list.getItems().get(0); + return BizConvertUtils.toBizJob(job); + }catch (KubernetesClientException e) { + LogUtil.error(LogEnum.BIZ_K8S, "TrainJobApiImpl.get error, param:[namespace]={}, [resourceName]={}, error:{}",namespace, resourceName,e); + return new BizJob().error(String.valueOf(e.getCode()),e.getMessage()); + } + } + + private class JupyterDeployer{ + private String baseName; + private String jobName; + + private String namespace; + private String image; + private Boolean useGpu; + private List cmdLines; + private Map fsMounts; + + private Map resourcesLimitsMap; + private Map baseLabels; + + private List volumeMounts; + private List volumes; + private String businessLabel; + private Integer delayCreate; + private Integer delayDelete; + private TaskYamlBO taskYamlBO; + private String errCode; + private String errMessage; + + private JupyterDeployer(PtJupyterJobBO bo){ + this.baseName = bo.getName(); + this.jobName = StrUtil.format(K8sParamConstants.RESOURCE_NAME_TEMPLATE, baseName, RandomUtil.randomString(K8sParamConstants.RESOURCE_NAME_SUFFIX_LENGTH)); + this.namespace = bo.getNamespace(); + this.image = bo.getImage(); + this.cmdLines = new ArrayList(); + Optional.ofNullable(bo.getCmdLines()).ifPresent(v -> cmdLines = v); + this.useGpu = bo.getUseGpu()==null?false:bo.getUseGpu(); + if (bo.getUseGpu() != null && bo.getUseGpu() && null == bo.getGpuNum()){ + bo.setGpuNum(MagicNumConstant.ZERO); + } + + this.resourcesLimitsMap = Maps.newHashMap(); + Optional.ofNullable(bo.getCpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_CPU_KEY, new Quantity(v.toString(), K8sParamConstants.CPU_UNIT))); + Optional.ofNullable(bo.getGpuNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.GPU_RESOURCE_KEY, new Quantity(v.toString()))); + Optional.ofNullable(bo.getMemNum()).ifPresent(v -> resourcesLimitsMap.put(K8sParamConstants.QUANTITY_MEMORY_KEY, new Quantity(v.toString(), K8sParamConstants.MEM_UNIT))); + + this.fsMounts = bo.getFsMounts(); + businessLabel = bo.getBusinessLabel(); + this.baseLabels = LabelUtils.getBaseLabels(baseName,bo.getBusinessLabel()); + + this.volumeMounts = new ArrayList<>(); + this.volumes = new ArrayList<>(); + this.delayCreate = bo.getDelayCreateTime(); + this.delayDelete = bo.getDelayDeleteTime(); + this.taskYamlBO = new TaskYamlBO(); + this.errCode = K8sResponseEnum.SUCCESS.getCode(); + this.errMessage = SymbolConstant.BLANK; + } + + /** + * 部署Job + * + * @return PtJupyterJobVO 训练任务 Job 结果类 + */ + public PtJupyterJobVO deploy() { + delayCreate = delayCreate == null || delayCreate <= 0 ? MagicNumConstant.ZERO : delayCreate; + delayDelete = delayDelete == null || delayDelete <= 0 ? MagicNumConstant.ZERO : delayDelete; + if (!K8sResponseEnum.SUCCESS.getCode().equals(errCode)){ + return new PtJupyterJobVO().error(errCode,errMessage); + } + //部署job + Job job = deployJob(delayCreate, delayDelete); + if (CollectionUtil.isNotEmpty(taskYamlBO.getYamlList()) && (delayCreate > MagicNumConstant.ZERO || delayDelete > MagicNumConstant.ZERO)){ + long applyUnixTime = System.currentTimeMillis()/MagicNumConstant.THOUSAND_LONG + delayCreate*MagicNumConstant.SIXTY_LONG; + Timestamp applyDisplayTime = new Timestamp(applyUnixTime * MagicNumConstant.THOUSAND_LONG); + long stopUnixTime = applyUnixTime + delayDelete* MagicNumConstant.SIXTY_LONG; + Timestamp stopDisplayTime = new Timestamp(stopUnixTime * MagicNumConstant.THOUSAND_LONG); + K8sTask k8sTask = new K8sTask(){{ + setNamespace(namespace); + setResourceName(baseName); + setTaskYaml(JSON.toJSONString(taskYamlBO)); + setBusiness(businessLabel); + setApplyUnixTime(applyUnixTime); + setApplyDisplayTime(applyDisplayTime); + setApplyStatus(delayCreate == MagicNumConstant.ZERO ? MagicNumConstant.ZERO : MagicNumConstant.ONE); + }}; + if (delayDelete > MagicNumConstant.ZERO){ + k8sTask.setStopUnixTime(stopUnixTime); + k8sTask.setStopDisplayTime(stopDisplayTime); + k8sTask.setStopStatus(MagicNumConstant.ONE); + } + k8sTaskService.createOrUpdateTask(k8sTask); + } + + return PtJupyterJobVO.getInstance(job); + } + + /** + * 挂载存储 + * + * @return JupyterDeployer Jupyter Job 部署类 + */ + private JupyterDeployer buildVolumes(){ + + // 针对于共享内存挂载存储 + buildSharedMemoryVolume(); + + if (CollectionUtil.isNotEmpty(fsMounts)){ + int i = MagicNumConstant.ZERO; + for (Map.Entry mount : fsMounts.entrySet()) { + boolean availableMount = (mount != null && StringUtils.isNotEmpty(mount.getKey()) && mount.getValue() != null && StringUtils.isNotEmpty(mount.getValue().getDir())); + if (availableMount){ + boolean success = mount.getValue().isRecycle()?buildFsPvcVolumes(mount.getKey(),mount.getValue(),i):buildFsVolumes(mount.getKey(),mount.getValue(),i); + if (!success){ + break; + } + i++; + } + } + } + return this; + } + + /** + * 针对于共享内存挂载存储 + * + */ + private void buildSharedMemoryVolume(){ + volumeMounts.add(new VolumeMountBuilder() + .withName(K8sParamConstants.SHM_NAME) + .withMountPath(K8sParamConstants.SHM_MOUNTPATH) + .build()); + volumes.add(new VolumeBuilder() + .withName(K8sParamConstants.SHM_NAME) + .withNewEmptyDir() + .withMedium(K8sParamConstants.SHM_MEDIUM) + .endEmptyDir() + .build()); + } + + /** + * 挂载存储 + * + * @param mountPath 挂载路径 + * @param dirBO 挂载路径参数 + * @param num 名称序号 + * @return boolean true成功 false失败 + */ + private boolean buildFsVolumes(String mountPath,PtMountDirBO dirBO,int num){ + volumeMounts.add(new VolumeMountBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+num) + .withMountPath(mountPath) + .withReadOnly(dirBO.isReadOnly()) + .build()); + volumes.add(new VolumeBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+num) + .withNewHostPath() + .withPath(dirBO.getDir()) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + return true; + } + + /** + * 按照存储资源声明挂载存储 + * + * @param mountPath 挂载路径 + * @param dirBO 挂载路径参数 + * @param i 名称序号 + * @return boolean true成功 false失败 + */ + private boolean buildFsPvcVolumes(String mountPath,PtMountDirBO dirBO,int i){ + BizPersistentVolumeClaim bizPersistentVolumeClaim = persistentVolumeClaimApi.createWithFsPv(new PtPersistentVolumeClaimBO(namespace,baseName,dirBO)); + if (bizPersistentVolumeClaim.isSuccess()){ + volumeMounts.add(new VolumeMountBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+i) + .withMountPath(mountPath) + .withReadOnly(dirBO.isReadOnly()) + .build()); + volumes.add(new VolumeBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+i) + .withNewPersistentVolumeClaim(bizPersistentVolumeClaim.getName(), dirBO.isReadOnly()) + .build()); + return true; + }else { + this.errCode = bizPersistentVolumeClaim.getCode(); + this.errMessage = bizPersistentVolumeClaim.getMessage(); + } + return false; + } + + /** + * 部署Job + * + * @param delayCreate 创建 + * @param delayDelete 删除 + * @return Job job类 + */ + private Job deployJob(Integer delayCreate, Integer delayDelete) { + + Job job = null; + JobList list = client.batch().jobs().inNamespace(namespace).withLabels(LabelUtils.withEnvResourceName(baseName)).list(); + if(CollectionUtil.isNotEmpty(list.getItems())){ + job = list.getItems().get(0); + jobName = job.getMetadata().getName(); + boolean succeedOrFailed = (job.getStatus().getSucceeded() != null && job.getStatus().getSucceeded() > 0) || (job.getStatus().getFailed() != null && job.getStatus().getFailed() > 0); + if(succeedOrFailed){ + LogUtil.info(LogEnum.BIZ_K8S, "Delete existing job {}", jobName); + client.resource(job).delete(); + }else{ + LogUtil.info(LogEnum.BIZ_K8S, "Skip creating job, {} already exists", jobName); + return job; + } + } + //容器 + Container container = new Container(); + + //环境变量 + List env = new ArrayList(); + env.add(new EnvVarBuilder().withName(K8sParamConstants.PYTHONUNBUFFERED).withValue(SymbolConstant.ZERO).build()); + container.setEnv(env); + + //镜像 + container.setName(jobName); + container.setImage(image); + container.setImagePullPolicy(ImagePullPolicyEnum.IFNOTPRESENT.getPolicy()); + container.setVolumeMounts(volumeMounts); + //启动命令 + container.setCommand(Collections.singletonList(ShellCommandEnum.BIN_BANSH.getShell())); + container.setArgs(cmdLines); + + //资源限制 + container.setResources(new ResourceRequirementsBuilder() + .addToLimits(resourcesLimitsMap) + .build()); + + Map gpuLabel = new HashMap(1); + if (useGpu){ + gpuLabel.put(K8sLabelConstants.NODE_GPU_LABEL_KEY,K8sLabelConstants.NODE_GPU_LABEL_VALUE); + } + + job = new JobBuilder() + .withNewMetadata() + .withName(jobName) + .addToLabels(baseLabels) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .withParallelism(1) + .withCompletions(1) + .withBackoffLimit(0) + .withNewTemplate() + .withNewMetadata() + .withName(jobName) + .addToLabels(LabelUtils.getChildLabels(baseName, jobName, K8sKindEnum.JOB.getKind(),businessLabel)) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .withTerminationGracePeriodSeconds(MagicNumConstant.ZERO_LONG) + .addToNodeSelector(gpuLabel) + .addToContainers(container) + .addToVolumes(volumes.toArray(new Volume[0])) + .withRestartPolicy(RestartPolicyEnum.NEVER.getRestartPolicy()) + .endSpec() + .endTemplate() + .endSpec() + .build(); + if (delayCreate == null || delayCreate == MagicNumConstant.ZERO){ + resourceIisolationApi.addIisolationInfo(job); + LogUtil.info(LogEnum.BIZ_K8S, "Ready to deploy {}", jobName); + job = client.batch().jobs().create(job); + LogUtil.info(LogEnum.BIZ_K8S, "{} deployed successfully", jobName); + } + if (delayCreate > MagicNumConstant.ZERO || delayDelete > MagicNumConstant.ZERO){ + taskYamlBO.append(job); + } + + return job; + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/VolumeApiImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/VolumeApiImpl.java new file mode 100644 index 0000000..003bbbb --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/api/impl/VolumeApiImpl.java @@ -0,0 +1,128 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.api.impl; + +import cn.hutool.core.collection.CollectionUtil; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.k8s.api.PersistentVolumeClaimApi; +import org.dubhe.k8s.api.VolumeApi; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.bo.BuildFsVolumeBO; +import org.dubhe.k8s.domain.bo.PtMountDirBO; +import org.dubhe.k8s.domain.bo.PtPersistentVolumeClaimBO; +import org.dubhe.k8s.domain.resource.BizPersistentVolumeClaim; +import org.dubhe.k8s.domain.vo.VolumeVO; +import org.dubhe.biz.base.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * @description Kubernetes Volume api implements + * @date 2020-09-10 + */ +@Service +public class VolumeApiImpl implements VolumeApi { + @Autowired + private PersistentVolumeClaimApi persistentVolumeClaimApi; + + /** + * 构建文件存储服务存储卷 + * + * @param bo 文件存储服务存储卷参数 + * @return + */ + @Override + public VolumeVO buildFsVolumes(BuildFsVolumeBO bo) { + if (bo == null){ + return new VolumeVO().errorBadRequest(); + } + VolumeVO volumeVO = new VolumeVO(); + if (CollectionUtil.isNotEmpty(bo.getFsMounts())){ + int i = MagicNumConstant.ZERO; + for (Map.Entry mount : bo.getFsMounts().entrySet()) { + boolean availableMount = (mount != null && StringUtils.isNotEmpty(mount.getKey()) && mount.getValue() != null && StringUtils.isNotEmpty(mount.getValue().getDir())); + if (availableMount){ + boolean success = mount.getValue().isRecycle()?buildFsPvcVolumes(bo,volumeVO,mount.getKey(),mount.getValue(),i):buildFsVolumes(volumeVO,mount.getKey(),mount.getValue(),i); + if (!success){ + break; + } + i++; + } + } + } + return volumeVO; + } + + /** + * 构建存储卷 + * + * @param mountPath 挂载路径 + * @param dirBO 挂载路径参数 + * @param num 名称序号 + * @return boolean + */ + private boolean buildFsVolumes(VolumeVO volumeVO, String mountPath, PtMountDirBO dirBO, int num){ + if (volumeVO == null || StringUtils.isEmpty(mountPath) || dirBO == null){ + return false; + } + volumeVO.addVolumeMount(new VolumeMountBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+num) + .withMountPath(mountPath) + .withReadOnly(dirBO.isReadOnly()) + .build()); + volumeVO.addVolume(new VolumeBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+num) + .withNewHostPath() + .withPath(dirBO.getDir()) + .withType(K8sParamConstants.HOST_PATH_TYPE) + .endHostPath() + .build()); + return true; + } + + /** + * 按照存储资源声明挂载存储 + * + * @param mountPath 挂载路径 + * @param dirBO 挂载路径参数 + * @param i 名称序号 + * @return boolean + */ + private boolean buildFsPvcVolumes(BuildFsVolumeBO bo, VolumeVO volumeVO, String mountPath, PtMountDirBO dirBO, int i){ + BizPersistentVolumeClaim bizPersistentVolumeClaim = persistentVolumeClaimApi.createWithFsPv(new PtPersistentVolumeClaimBO(bo.getNamespace(),bo.getResourceName(),dirBO)); + if (bizPersistentVolumeClaim.isSuccess()){ + volumeVO.addVolumeMount(new VolumeMountBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+i) + .withMountPath(mountPath) + .withReadOnly(dirBO.isReadOnly()) + .build()); + volumeVO.addVolume(new VolumeBuilder() + .withName(K8sParamConstants.VOLUME_PREFIX+i) + .withNewPersistentVolumeClaim(bizPersistentVolumeClaim.getName(), dirBO.isReadOnly()) + .build()); + return true; + }else { + volumeVO.error(bizPersistentVolumeClaim.getCode(),bizPersistentVolumeClaim.getMessage()); + } + return false; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/aspect/ValidationAspect.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/aspect/ValidationAspect.java new file mode 100644 index 0000000..f296eea --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/aspect/ValidationAspect.java @@ -0,0 +1,166 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.aspect; + +import cn.hutool.core.util.ArrayUtil; +import com.alibaba.fastjson.JSON; +import lombok.Data; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.enums.ValidationTypeEnum; +import org.dubhe.k8s.utils.ValidationUtils; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +/** + * @description 参数校验 + * AOP org.dubhe.k8s.api.impl下的所有方法加了@K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) 的参数和参数内的一级字段会被校验 + * @date 2020-06-04 + */ +@Component +@Aspect +public class ValidationAspect { + @Order(1) + @Around("execution(* org.dubhe.k8s.api.impl.*.*(..)))") + public Object k8sResourceNameValidation(JoinPoint point) throws Throwable { + /**获取参数注解**/ + MethodSignature methodSignature = (MethodSignature) point.getSignature(); + Annotation[][] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations(); + /**参数**/ + Object[] args = point.getArgs(); + if (ArrayUtil.isNotEmpty(args)) { + for (int i = 0; i < parameterAnnotations.length; i++) { + if (null == args[i]) { + continue; + } + /**基本类型不做校验**/ + if (args[i].getClass().isPrimitive()) { + continue; + } + if (args[i] instanceof String) { + /**对String类型做校验**/ + for (Annotation annotation : parameterAnnotations[i]) { + if (annotation instanceof K8sValidation && ValidationTypeEnum.K8S_RESOURCE_NAME.equals(((K8sValidation) annotation).value())) { + ValidateResourceNameResult validateResult = validateResourceName((String) args[i]); + if (!validateResult.isSuccess()) { + return getValidationResourceNameErrorReturn(methodSignature.getReturnType(), validateResult.getField()); + } + } + } + } else { + /**对非String类型做其字段校验**/ + ValidateResourceNameResult validateResult = validateArgResourceName(args[i], args[i].getClass()); + if (!validateResult.isSuccess()) { + return getValidationResourceNameErrorReturn(methodSignature.getReturnType(), validateResult.getField()); + } + } + } + } + return ((ProceedingJoinPoint) point).proceed(); + } + + /** + * 校验k8s资源对象名称是否合法 + * + * @param resourceName 资源名称 + * @return ValidateResourceNameResult 校验资源名称结果类 + */ + private ValidateResourceNameResult validateResourceName(String resourceName) { + return new ValidateResourceNameResult(ValidationUtils.validateResourceName(resourceName), resourceName); + } + + /** + * 对参数内部字段做k8s资源对象名称是否合法校验 + * + * @param arg 任意对象 + * @param argClass Class类对象 + * @return ValidateResourceNameResult 校验资源名称结果类 + */ + private ValidateResourceNameResult validateArgResourceName(Object arg, Class argClass) { + Field[] fields = argClass.getDeclaredFields(); + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + K8sValidation k8sValidation = field.getDeclaredAnnotation(K8sValidation.class); + if (k8sValidation == null) { + continue; + } + if (ValidationTypeEnum.K8S_RESOURCE_NAME.equals(k8sValidation.value()) && field.getType().equals(String.class)) { + field.setAccessible(true); + try { + String resourceName = (String) field.get(arg); + if (!validateResourceName(resourceName).isSuccess()) { + return new ValidateResourceNameResult(false, resourceName); + } + } catch (IllegalAccessException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ValidationAspect.validateArgResourceName exception, param:[arg]={}, [argClass]={}, exception:{}", JSON.toJSONString(arg), JSON.toJSONString(argClass), e); + } + } + } + /**递归校验父类属性**/ + if (argClass.getSuperclass() != Object.class) { + return validateArgResourceName(arg, argClass.getSuperclass()); + } + return new ValidateResourceNameResult(true, null); + } + + /** + * 校验不通过获取返回值 + * + * @param returnType Class类对象 + * @param fieldName 字段名称 + * @return Object 任意对象 + */ + private Object getValidationResourceNameErrorReturn(Class returnType, String fieldName) { + /**获取返回值类型**/ + try { + if (PtBaseResult.class.isAssignableFrom(returnType)) { + PtBaseResult validationReturn = (PtBaseResult) returnType.newInstance(); + return validationReturn.validationErrorRequest(fieldName); + } + } catch (InstantiationException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ValidationAspect.getValidationResourceNameErrorReturn exception, param:[returnType]={}, [fieldName]={}", JSON.toJSONString(returnType), fieldName, e); + } catch (IllegalAccessException e) { + LogUtil.error(LogEnum.BIZ_K8S, "ValidationAspect.getValidationResourceNameErrorReturn exception, param:[returnType]={}, [fieldName]={}", JSON.toJSONString(returnType), fieldName, e); + } + return null; + } + + /** + * 校验结果 + */ + @Data + private class ValidateResourceNameResult { + private boolean success; + private String field; + + public ValidateResourceNameResult(boolean success, String field) { + this.success = success; + this.field = field; + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/cache/ResourceCache.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/cache/ResourceCache.java new file mode 100644 index 0000000..577b59a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/cache/ResourceCache.java @@ -0,0 +1,238 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.cache; + +import cn.hutool.core.collection.CollectionUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.redis.utils.RedisUtils; +import org.dubhe.k8s.api.PodApi; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.domain.entity.K8sResource; +import org.dubhe.k8s.domain.resource.BizPod; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.service.K8sResourceService; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.CollectionUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @description k8s资源缓存 + * @date 2020-05-20 + */ +public class ResourceCache { + @Autowired + private RedisUtils redisUtils; + @Autowired + private PodApi podApi; + @Autowired + private K8sResourceService k8sResourceService; + + /**podNmae/resourceName缓存失效时间 单位秒**/ + private static final Long TIME_OUT = 7*24*3600L; + /**缓存击穿前缀**/ + @Value("K8sClient:Pod:"+"${spring.profiles.active}_cache_breakdown_") + private String cacheBreakdownPrefix; + /**缓存穿透标记过期时间**/ + private static final Integer CACHE_BREAKDOWN = 30; + + @Value("K8sClient:Pod:"+"${spring.profiles.active}_k8s_pod_resourcename_") + private String resourceNamePrefix; + + @Value("K8sClient:Pod:"+"${spring.profiles.active}_k8s_pod_name_") + private String podNamePrefix; + + /** + * 设置资源名称到 pod名称的缓存 + * + * @param resourceName 资源名称 + * @param podName Pod名称 + * @return boolean true 缓存 false 不缓存 + */ + public boolean cachePod(String resourceName,String podName){ + try{ + Boolean success = redisUtils.zSet(resourceNamePrefix +resourceName,TIME_OUT+ThreadLocalRandom.current().nextLong(MagicNumConstant.ZERO,MagicNumConstant.ONE_HUNDRED),podName); + if (success){ + return redisUtils.set(podNamePrefix +podName,resourceName,TIME_OUT+ThreadLocalRandom.current().nextLong(MagicNumConstant.ZERO,MagicNumConstant.ONE_HUNDRED)); + } + return false; + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_K8S,"Cache exception, resourceName: {}, podName: {}, exception information: {}",resourceName,podName,e); + return false; + } + } + + /** + * 设置资源名称到 pod名称的缓存 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return boolean true 缓存 false 不缓存 + */ + public boolean cachePods(String namespace,String resourceName){ + try{ + List podList = podApi.getListByResourceName(namespace,resourceName); + if (CollectionUtil.isNotEmpty(podList)){ + podList.forEach(pod->{ + cachePod(resourceName,pod.getName()); + k8sResourceService.create(pod); + }); + } + return true; + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_K8S,"Cache exception, namespace: {}, resourceName: {}, exception information: {}",namespace,resourceName,e); + return false; + } + } + + /** + * 从缓存中取 resourceName对应的podName + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return Set podName的集合 + */ + public Set getPodNameByResourceName(String namespace,String resourceName){ + try{ + /**缓存穿透**/ + if (redisUtils.get(cacheBreakdownPrefix +resourceName) != null){ + return null; + } + Set set = (Set) redisUtils.zGet(resourceNamePrefix +resourceName); + if (CollectionUtil.isEmpty(set) && StringUtils.isNotEmpty(namespace)){ + List bizPods = podApi.getListByResourceName(namespace,resourceName); + Set finalSet = new HashSet<>(MagicNumConstant.ONE); + if(CollectionUtil.isNotEmpty(bizPods)){ + bizPods.forEach(obj-> { + finalSet.add(obj.getName()); + cachePod(resourceName,obj.getName()); + }); + }else { + List k8sResourceList = k8sResourceService.selectByResourceName(K8sKindEnum.POD.getKind(),namespace,resourceName); + if (CollectionUtil.isNotEmpty(k8sResourceList)){ + k8sResourceList.forEach(obj->{ + finalSet.add(obj.getName()); + cachePod(resourceName,obj.getName()); + }); + }else { + /**设置缓存穿透标记**/ + redisUtils.set(cacheBreakdownPrefix +resourceName, cacheBreakdownPrefix +resourceName,CACHE_BREAKDOWN); + } + } + return finalSet; + } + return set; + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_K8S,"Query cache exception, namespace: {}, resourceName: {}, exception information: {}",namespace,resourceName,e); + return new HashSet<>(); + } + } + + /** + * 从缓存中取 podName对应的resourceName + * + * @param namespace 命名空间 + * @param podName Pod的名称 + * @return String resourceName的名称 + */ + public String getResourceNameByPodName(String namespace,String podName){ + try{ + /**缓存穿透**/ + if (redisUtils.get(cacheBreakdownPrefix +podName) != null){ + return null; + } + String resourceName = (String) redisUtils.get(podNamePrefix +podName); + if (StringUtils.isEmpty(resourceName) && StringUtils.isNotEmpty(namespace)){ + BizPod pod = podApi.get(namespace,podName); + if (pod != null){ + resourceName = pod.getLabels().get(K8sLabelConstants.BASE_TAG_SOURCE); + cachePod(resourceName,podName); + return resourceName; + }else { + List k8sResourceList = k8sResourceService.selectByName(K8sKindEnum.POD.getKind(),namespace,podName); + if (CollectionUtil.isNotEmpty(k8sResourceList)){ + resourceName = k8sResourceList.get(0).getResourceName(); + cachePod(resourceName,podName); + }else { + redisUtils.set(cacheBreakdownPrefix +podName, cacheBreakdownPrefix +podName,CACHE_BREAKDOWN); + } + return resourceName; + } + } + return resourceName; + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_K8S,"Query cache exception, namespace: {}, podName: {}, exception information: {}",namespace,podName,e); + return null; + } + } + + /** + * 删除pod名称缓存 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return boolean true 删除成功 false 删除失败 + */ + public boolean deletePodCacheByResourceName(String namespace, String resourceName){ + try{ + if (StringUtils.isNotEmpty(namespace) && StringUtils.isNotEmpty(resourceName)){ + Set podNameSet = (Set) redisUtils.zGet(resourceNamePrefix +resourceName); + redisUtils.del(resourceNamePrefix +resourceName); + if (!CollectionUtils.isEmpty(podNameSet)){ + podNameSet.forEach(podName-> redisUtils.del(podNamePrefix +podName)); + } + k8sResourceService.deleteByResourceName(K8sKindEnum.POD.getKind(),namespace,resourceName); + } + return true; + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_K8S,"Delete cache exception, namespace: {}, resourceName: {}, exception information: {}",namespace,resourceName,e); + return false; + } + } + + /** + * 删除pod名称缓存 + * + * @param namespace 命名空间 + * @param podName Pod名称 + * @return boolean true 删除成功 false删除失败 + */ + public boolean deletePodCacheByPodName(String namespace, String podName){ + try { + if (StringUtils.isNotEmpty(namespace) && StringUtils.isNotEmpty(podName)){ + String resourceName = (String) redisUtils.get(podNamePrefix +podName); + redisUtils.del(podNamePrefix + podName); + if (StringUtils.isNotEmpty(resourceName)){ + redisUtils.del(resourceNamePrefix +resourceName); + } + k8sResourceService.deleteByName(K8sKindEnum.POD.getKind(),namespace,podName); + } + return true; + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_K8S,"Delete cache exception, namespace: {}, podName: {}, exception information: {}",namespace,podName,e); + return false; + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sCallbackMvcConfig.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sCallbackMvcConfig.java new file mode 100644 index 0000000..8bf53ea --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sCallbackMvcConfig.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.config; + + +import org.dubhe.k8s.interceptor.K8sCallBackPodInterceptor; +import org.dubhe.k8s.utils.K8sCallBackTool; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; + +/** + * @description Web Mvc Config + * @date 2020-05-28 + */ +@Configuration +public class K8sCallbackMvcConfig implements WebMvcConfigurer { + + @Resource + private K8sCallBackPodInterceptor k8sCallBackPodInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + InterceptorRegistration registration = registry.addInterceptor(k8sCallBackPodInterceptor); + // 拦截配置 + registration.addPathPatterns(K8sCallBackTool.getK8sCallbackPaths()); + + } + + + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sConfig.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sConfig.java new file mode 100644 index 0000000..e01b988 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/config/K8sConfig.java @@ -0,0 +1,178 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.config; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.*; +import org.dubhe.k8s.api.impl.*; +import org.dubhe.k8s.cache.ResourceCache; +import org.dubhe.k8s.properties.ClusterProperties; +import org.dubhe.k8s.utils.K8sUtils; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.elasticsearch.client.RestHighLevelClient; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +import static org.apache.http.HttpVersion.HTTP; +import static org.dubhe.biz.base.constant.MagicNumConstant.TEN_THOUSAND; +import static org.dubhe.biz.base.constant.MagicNumConstant.ZERO; +import static org.dubhe.biz.base.constant.SymbolConstant.COLON; +import static org.dubhe.biz.base.constant.SymbolConstant.COMMA; + +/** + * @description load kubeconfig + * @date 2020-04-09 + */ +@Configuration +@EnableConfigurationProperties(ClusterProperties.class) +@MapperScan("org.dubhe.k8s.dao") +public class K8sConfig { + + @Autowired + private ClusterProperties clusterProperties; + + @Value("${k8s.elasticsearch.hostlist}") + private String hostlist; + + @Bean + public K8sUtils k8sUtils() throws IOException { + LogUtil.debug(LogEnum.BIZ_K8S, "ClusterProperties======{}", JSONObject.toJSONString(clusterProperties)); + if (clusterProperties == null) { + return null; + } + final String kubeconfig = clusterProperties.getKubeconfig(); + LogUtil.debug(LogEnum.BIZ_K8S, "ClusterProperties.getKubeconfig()======{}", clusterProperties.getKubeconfig()); + final String url = clusterProperties.getUrl(); + LogUtil.debug(LogEnum.BIZ_K8S, "ClusterProperties.getUrl()======{}", clusterProperties.getKubeconfig()); + if (StrUtil.isEmpty(url) && StrUtil.isEmpty(kubeconfig)) { + return null; + } + return new K8sUtils(clusterProperties); + } + + @Bean + public JupyterResourceApi jupyterResourceApi(K8sUtils k8sUtils) { + return new JupyterResourceApiImpl(k8sUtils); + } + + @Bean + public TrainJobApi jupyterJobApi(K8sUtils k8sUtils) { + return new TrainJobApiImpl(k8sUtils); + } + + @Bean + public PodApi podApi(K8sUtils k8sUtils) { + return new PodApiImpl(k8sUtils); + } + + @Bean + public NamespaceApi namespaceApi(K8sUtils k8sUtils) { + return new NamespaceApiImpl(k8sUtils); + } + + @Bean + public NodeApi nodeApi(K8sUtils k8sUtils) { + return new NodeApiImpl(k8sUtils); + } + + @Bean + public LimitRangeApi limitRangeApi(K8sUtils k8sUtils) { + return new LimitRangeApiImpl(k8sUtils); + } + + @Bean + public ResourceQuotaApi resourceQuotaApi(K8sUtils k8sUtils) { + return new ResourceQuotaApiImpl(k8sUtils); + } + + @Bean + public PersistentVolumeClaimApi persistentVolumeClaimApi(K8sUtils k8sUtils) { + return new PersistentVolumeClaimApiImpl(k8sUtils); + } + + @Bean + public LogMonitoringApi logMonitoringApi(K8sUtils k8sUtils) { + return new LogMonitoringApiImpl(k8sUtils); + } + + @Bean + public ResourceCache resourceCache() { + return new ResourceCache(); + } + + @Bean + public MetricsApi metricsApi(K8sUtils k8sUtils) { + return new MetricsApiImpl(k8sUtils); + } + + @Bean + public NativeResourceApi nativeResourceApi(K8sUtils k8sUtils) { + return new NativeResourceApiImpl(k8sUtils); + } + + @Bean + public DubheDeploymentApi dubheDeploymentApi(K8sUtils k8sUtils) { + return new DubheDeploymentApiImpl(k8sUtils); + } + + @Bean + public ModelOptJobApi jobApi(K8sUtils k8sUtils) { + return new ModelOptJobApiImpl(k8sUtils); + } + + @Bean + public DistributeTrainApi distributeTrainApi(K8sUtils k8sUtils) { + return new DistributeTrainApiImpl(k8sUtils); + } + + @Bean + public RestHighLevelClient restHighLevelClient(){ + + String[] hosts = hostlist.split(COMMA); + HttpHost[] httpHostArray = new HttpHost[hosts.length]; + for(int i=ZERO;i { +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/dao/K8sTaskMapper.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/dao/K8sTaskMapper.java new file mode 100644 index 0000000..55f2024 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/dao/K8sTaskMapper.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; +import org.dubhe.k8s.domain.bo.K8sTaskBO; +import org.dubhe.k8s.domain.entity.K8sTask; + +import java.util.List; + +/** + * @description k8s任务mapper接口 + * @date 2020-8-31 + */ +public interface K8sTaskMapper extends BaseMapper { + + /** + * 保存任务 + * + * @param k8sTask k8s任务类 + */ + int insertOrUpdate(K8sTask k8sTask); + + /** + * 查询待执行任务 + * + * @param k8sTaskBO k8s任务查询类 + * @return List k8s任务集合类 + */ + List selectUnexecutedTask(K8sTaskBO k8sTaskBO); + + /** + * 根据namespace 和 resourceName 删除 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return boolean + */ + @Update("update k8s_task set deleted = #{deleteFlag} where namespace = #{namespace} and resource_name = #{resourceName}") + int deleteByNamespaceAndResourceName(@Param("namespace") String namespace,@Param("resourceName") String resourceName, @Param("deleteFlag") boolean deleteFlag); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/PtBaseResult.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/PtBaseResult.java new file mode 100644 index 0000000..701bde3 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/PtBaseResult.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.enums.K8sResponseEnum; + +import java.io.Serializable; + +/** + * @description base result + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class PtBaseResult implements Serializable { + private static final long serialVersionUID = 1L; + /** + * 错误码 符合kubernertes错误码 + **/ + private String code = "200"; + /** + * 错误信息 + **/ + private String message; + + public PtBaseResult() { + } + + public PtBaseResult(String code, String message) { + this.code = code; + this.message = message; + } + + public T error(String code, String message) { + this.code = code; + this.message = message; + return (T) this; + } + + public T errorBadRequest() { + this.code = K8sResponseEnum.BAD_REQUEST.getCode(); + this.message = K8sResponseEnum.BAD_REQUEST.getMessage(); + return (T) this; + } + + public T validationErrorRequest(String fieldName) { + this.code = K8sResponseEnum.PRECONDITION_FAILED.getCode(); + this.message = StrUtil.format("{} 字段校验失败,k8s资源命名规则必须符合 {},且长度不超过 {}", fieldName, K8sParamConstants.K8S_RESOURCE_NAME_REGEX, K8sParamConstants.RESOURCE_NAME_LENGTH); + return (T) this; + } + + public T baseErrorBadRequest() { + this.code = K8sResponseEnum.BAD_REQUEST.getCode(); + this.message = K8sResponseEnum.BAD_REQUEST.getMessage(); + return (T) this; + } + + public boolean isSuccess() { + return K8sResponseEnum.SUCCESS.getCode().equals(code); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildFsVolumeBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildFsVolumeBO.java new file mode 100644 index 0000000..63d0efd --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildFsVolumeBO.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Map; + +/** + * @description nfs存储卷 参数 + * @date 2020-09-10 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class BuildFsVolumeBO { + private String namespace; + private String resourceName; + private Map fsMounts; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildIngressBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildIngressBO.java new file mode 100644 index 0000000..259ea8b --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildIngressBO.java @@ -0,0 +1,121 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import io.fabric8.kubernetes.api.model.extensions.IngressRule; +import io.fabric8.kubernetes.api.model.extensions.IngressTLS; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.k8s.constant.K8sParamConstants; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @description 构建 Ingress + * @date 2020-09-11 + */ +@Data +@Accessors(chain = true) +public class BuildIngressBO { + /** + * 命名空间 + **/ + private String namespace; + /** + * 名称 + */ + private String name; + /** + * 标签 + */ + private Map labels; + /** + * 上传限制 + */ + private String maxUploadSize; + /** + * 根路径 + */ + private String path; + /** + * A list of host rules used to configure the Ingress + **/ + private List ingressRules; + /** + * TLS configuration + **/ + private List ingressTLSs; + + private Map annotations; + + public BuildIngressBO(String namespace, String name, Map labels){ + this.namespace = namespace; + this.name = name; + this.labels = labels; + this.maxUploadSize = K8sParamConstants.INGRESS_MAX_UPLOAD_SIZE; + this.path = SymbolConstant.SLASH; + } + + /** + * 添加ingress 规则 + * @param ingressRule + */ + public void addIngressRule(IngressRule ingressRule){ + if (null == ingressRule){ + return; + } + if (ingressRules == null){ + ingressRules = new ArrayList<>(); + } + ingressRules.add(ingressRule); + } + + /** + * 添加 ingress tls + * @param ingressTLS + */ + public void addIngressTLS(IngressTLS ingressTLS){ + if (null == ingressTLS){ + return; + } + if (ingressTLSs == null){ + ingressTLSs = new ArrayList<>(); + } + ingressTLSs.add(ingressTLS); + } + + /** + * 设置 annotation + * @param key + * @param value + */ + public void putAnnotation(String key,String value){ + if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)){ + return; + } + if (annotations == null){ + annotations = new HashMap<>(); + } + annotations.put(key,value); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildServiceBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildServiceBO.java new file mode 100644 index 0000000..b37e8f2 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/BuildServiceBO.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import io.fabric8.kubernetes.api.model.ServicePort; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @description 构建 Service + * @date 2020-09-11 + */ +@Data +@Accessors(chain = true) +public class BuildServiceBO { + private String namespace; + private String name; + private Map labels; + private Map selector; + private List ports; + + public BuildServiceBO(String namespace, String name, Map labels, Map selector){ + this.namespace = namespace; + this.name = name; + this.labels = labels; + this.selector = selector; + } + + /** + * 添加端口 + * @param port + */ + public void addPort(ServicePort port){ + if (ports == null){ + ports = new ArrayList<>(); + } + ports.add(port); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/DistributeTrainBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/DistributeTrainBO.java new file mode 100644 index 0000000..c8565bd --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/DistributeTrainBO.java @@ -0,0 +1,142 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.enums.ValidationTypeEnum; +import org.dubhe.biz.base.utils.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @description DistributeTrainBO + * @date 2020-07-08 + */ +@Data +@Accessors(chain = true) +public class DistributeTrainBO { + /** + * 命名空间 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + /** + * 资源名称 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String name; + /** + * 多机启动实例数 + **/ + private Integer size; + /** + * 镜像名称 + **/ + private String image; + /** + * master机器运行时执行命令 + **/ + private String masterCmd; + /** + * 每个节点运行使用内存,单位Mi + **/ + private Integer memNum; + /** + * 每个节点运行使用CPU数量,1000代表占用一核心 + **/ + private Integer cpuNum; + /** + * 每个节点运行使用GPU数量 + **/ + private Integer gpuNum; + /** + * slave机器运行时执行命令 + **/ + private String slaveCmd; + /** + * 运行环境变量 + **/ + private Map env; + /** + * 业务标签,用于标识业务模块 + **/ + private String businessLabel; + /** + * 延时创建时间,单位:分钟 + ***/ + private Integer delayCreateTime; + /** + * 定时删除时间,相对于实际创建时间,单位:分钟 + **/ + private Integer delayDeleteTime; + /** + * 文件存储服务挂载 key:pod内挂载路径 value:文件存储路径及配置 + **/ + private Map fsMounts; + + /** + * 设置文件存储挂载 + * @param mountPath pod内挂载路径 + * @param dir 文件存储服务路径 + * @return + */ + public DistributeTrainBO putFsMounts(String mountPath,String dir){ + if (StringUtils.isNotEmpty(mountPath) && StringUtils.isNotEmpty(dir)){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.EIGHT); + } + fsMounts.put(mountPath,new PtMountDirBO(dir)); + } + return this; + } + + /** + * 设置文件存储挂载 + * @param mountPath pod内挂载路径 + * @param dir 文件存储服务路径及配置 + * @return + */ + public DistributeTrainBO putFsMounts(String mountPath,PtMountDirBO dir){ + if (StringUtils.isNotEmpty(mountPath) && dir != null){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.EIGHT); + } + fsMounts.put(mountPath,dir); + } + return this; + } + + /** + * 获取 文件存储服务路径列表 + * @return + */ + public List getDirList(){ + if (CollectionUtil.isNotEmpty(fsMounts)){ + return fsMounts.values().stream().map(PtMountDirBO::getDir).collect(Collectors.toList()); + } + return new ArrayList<>(); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/K8sTaskBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/K8sTaskBO.java new file mode 100644 index 0000000..02859a4 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/K8sTaskBO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.k8s.domain.entity.K8sTask; + +/** + * @description K8sTaskBO k8s任务参数 + * @date 2020-09-01 + */ +@Data +@NoArgsConstructor +public class K8sTaskBO extends K8sTask { + /** + * 最大创建时间 + */ + private Long maxApplyUnixTime; + /** + * 最大停止时间 + */ + private Long maxStopUnixTime; + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LabelBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LabelBO.java new file mode 100644 index 0000000..2dc7168 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LabelBO.java @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + * @description k8s resource的label + * @date 2020-04-14 + */ +@Data +@EqualsAndHashCode +public class LabelBO implements Map.Entry { + + private String key; + private String value; + + public LabelBO(String key, String value) { + this.key = key; + this.value = value; + } + + public static LabelBO of(String key, String value) { + return new LabelBO(key, value); + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String setValue(String value) { + String old = this.value; + this.value = value; + return old; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LogMonitoringBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LogMonitoringBO.java new file mode 100644 index 0000000..832261a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/LogMonitoringBO.java @@ -0,0 +1,74 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.domain.bo; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.domain.dto.PodLogQueryDTO; + +import java.util.Set; +/** + * @description LogMonitoringBO实体类 + * @date 2020-05-12 + */ +@Data +@Accessors(chain = true) +public class LogMonitoringBO { + /** + * 命名空间 + **/ + private String namespace; + /** + * 资源名称 + **/ + private String resourceName; + /** + * pod名称 + **/ + private String podName; + /** + * pod名称,一个resourceName可能对应多个podName + **/ + private Set podNames; + /** + * 日志查询条件:关键字 + **/ + private String logKeyword; + /** + * 日志查询时间范围:开始时间 + **/ + private Long beginTimeMillis; + /** + * 日志查询时间范围:结束时间 + **/ + private Long endTimeMillis; + + public LogMonitoringBO(String namespace,String podName){ + this.namespace = namespace; + this.podName = podName; + } + + public LogMonitoringBO(String namespace, PodLogQueryDTO podLogQueryDTO){ + this.namespace = namespace; + this.podName = podLogQueryDTO.getPodName(); + this.logKeyword = podLogQueryDTO.getLogKeyword(); + this.beginTimeMillis = podLogQueryDTO.getBeginTimeMillis(); + this.endTimeMillis = podLogQueryDTO.getEndTimeMillis(); + } + + public LogMonitoringBO(){ + } +} \ No newline at end of file diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ModelServingBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ModelServingBO.java new file mode 100644 index 0000000..dbbaa4c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ModelServingBO.java @@ -0,0 +1,135 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.enums.ValidationTypeEnum; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @description 模型部署 BO + * @date 2020-09-10 + */ +@Data +@Accessors(chain = true) +public class ModelServingBO { + /** + * 命名空间 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + /** + * 资源名称 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String resourceName; + /** + * Number of desired pods + */ + private Integer replicas; + /** + * GPU数量 + **/ + private Integer gpuNum; + /** + * 内存数量 单位Mi Gi + **/ + private Integer memNum; + /** + * CPU数量 + **/ + private Integer cpuNum; + /** + * 镜像名称 + **/ + private String image; + /** + * 执行命令 + **/ + private List cmdLines; + /** + * 文件存储服务挂载 key:pod内挂载路径 value:文件存储路径及配置 + **/ + private Map fsMounts; + /** + * 业务标签,用于标识业务模块 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String businessLabel; + /** + * http服务端口,null则不开放http服务 + */ + private Integer httpPort; + /** + * grpc服务端口,null则不开放grpc服务 + */ + private Integer grpcPort; + + /** + * 获取nfs路径 + * @return + */ + public List getDirList(){ + if (CollectionUtil.isNotEmpty(fsMounts)){ + return fsMounts.values().stream().map(PtMountDirBO::getDir).collect(Collectors.toList()); + } + return new ArrayList<>(); + } + + /** + * 设置nfs挂载 + * @param mountPath 容器内路径 + * @param dir nfs路径 + * @return + */ + public ModelServingBO putNfsMounts(String mountPath, String dir){ + if (StringUtils.isNotEmpty(mountPath) && StringUtils.isNotEmpty(dir)){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.TWO); + } + fsMounts.put(mountPath,new PtMountDirBO(dir)); + } + return this; + } + + /** + * 设置nfs挂载 + * @param mountPath 容器内路径 + * @param dir nfs路径及配置 + * @return + */ + public ModelServingBO putNfsMounts(String mountPath, PtMountDirBO dir){ + if (StringUtils.isNotEmpty(mountPath) && dir != null){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.TWO); + } + fsMounts.put(mountPath,dir); + } + return this; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PrometheusMetricBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PrometheusMetricBO.java new file mode 100644 index 0000000..5be6584 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PrometheusMetricBO.java @@ -0,0 +1,120 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; +import org.dubhe.biz.base.functional.StringFormat; +import org.dubhe.k8s.domain.vo.GpuUsageVO; +import org.dubhe.k8s.domain.vo.MetricsDataResultVO; +import org.dubhe.k8s.domain.vo.MetricsDataResultValueVO; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description Gpu 指标 BO + * @date 2020-10-13 + */ +@Data +public class PrometheusMetricBO { + private String status; + private MetricData data; + + /** + * 获取Gpu 使用率 + * @return List gpu使用列表 + */ + public List getGpuUsage(){ + List gpuUsageVOList = new ArrayList<>(); + if (data == null || CollectionUtils.isEmpty(data.getResult())){ + return gpuUsageVOList; + } + for (MetricResult result : data.getResult()){ + gpuUsageVOList.add(new GpuUsageVO(result.getMetric().getAcc_id(),Float.valueOf(result.getValue().get(1).toString()))); + } + return gpuUsageVOList; + } + + /** + * 获取value 列表 + * @return List 监控指标列表 + */ + public List getValues(StringFormat stringFormat){ + List list = new ArrayList<>(); + if (data == null || CollectionUtils.isEmpty(data.getResult())){ + return list; + } + for (MetricResult result : data.getResult()){ + result.getValues().forEach(obj->{ + list.add(new MetricsDataResultValueVO(obj.get(0).toString(),stringFormat.format(obj.get(1).toString()))); + }); + } + return list; + } + + /** + * 获取value 列表 + * @return List 监控指标列表 + */ + public List getValues(MetricResult metricResult){ + List list = new ArrayList<>(); + if (metricResult == null || CollectionUtils.isEmpty(metricResult.getValues())){ + return list; + } + metricResult.getValues().forEach(obj->{ + list.add(new MetricsDataResultValueVO(obj.get(0).toString(),obj.get(1).toString())); + }); + return list; + } + + /** + * 获取 result列表 + * @return List 监控指标列表 + */ + public List getResults(){ + List list = new ArrayList<>(); + if (data == null || CollectionUtils.isEmpty(data.getResult())){ + return list; + } + for (MetricResult result : data.getResult()){ + list.add(new MetricsDataResultVO(result.getMetric().getAcc_id(),getValues(result))); + } + return list; + } +} + +@Data +class MetricData { + private String resultType; + private List result; +} + +@Data +class MetricResult { + private Metric metric; + List value; + List> values; +} + +@Data +class Metric { + private String acc_id; + private String pod; +} + diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtDeploymentBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtDeploymentBO.java new file mode 100644 index 0000000..6805534 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtDeploymentBO.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.enums.ValidationTypeEnum; + +/** + * @description deployment bo + * @date 2020-05-26 + */ +@Data +public class PtDeploymentBO { + /** + * 命名空间 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + /** + * 资源名称 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String name; + /** + * GPU数量,0表示共享显卡,null表示不使用显卡 + **/ + private Integer gpuNum; + /** + * 内存数量单位 Mi + **/ + private Integer memNum; + /** + * CPU数量 1000代表占用一个核心 + **/ + private Integer cpuNum; + /** + * 镜像名称 + **/ + private String image; + /** + * 业务标签,用于标识业务模块 + **/ + private String businessLabel; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJobBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJobBO.java new file mode 100644 index 0000000..e48dd7e --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJobBO.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.enums.ValidationTypeEnum; + +/** + * @description job bo + * @date 2020-05-31 + */ +@Data +public class PtJobBO { + /** + * 命名空间 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + /** + * 资源名称 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String name; + /** + * GPU数量,0表示共享显卡,null表示不使用显卡 + **/ + private Integer gpuNum; + /** + * 内存数量单位 Mi + **/ + private Integer memNum; + /** + * CPU数量 1000代表占用一个核心 + **/ + private Integer cpuNum; + /** + * 镜像名称 + **/ + private String image; + /** + * 业务标签,用于标识业务模块 + **/ + private String businessLabel; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterJobBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterJobBO.java new file mode 100644 index 0000000..e22c499 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterJobBO.java @@ -0,0 +1,100 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.enums.GraphicsCardTypeEnum; +import org.dubhe.k8s.enums.ValidationTypeEnum; +import org.dubhe.biz.base.utils.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @description 训练任务 Job BO + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class PtJupyterJobBO { + /**命名空间**/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + /**资源名称**/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String name; + /**GPU数量,1代表使用一张显卡**/ + private Integer gpuNum; + /**是否使用gpu true:使用;false:不用**/ + private Boolean useGpu; + /**内存数量,单位Mi**/ + private Integer memNum; + /**cpu用量 单位:m 1个核心=1000m**/ + private Integer cpuNum; + /**镜像名称**/ + private String image; + /**执行命令**/ + private List cmdLines; + + /**文件存储服务挂载 key:pod内挂载路径 value:文件存储路径及配置**/ + private Map fsMounts; + + /**显卡类型**/ + private GraphicsCardTypeEnum graphicsCardType; + /**业务标签,用于标识业务模块**/ + private String businessLabel; + /**延时创建时间,单位:分钟**/ + private Integer delayCreateTime; + /**定时删除时间,相对于实际创建时间,单位:分钟**/ + private Integer delayDeleteTime; + + + public List getDirList(){ + if (CollectionUtil.isNotEmpty(fsMounts)){ + return fsMounts.values().stream().map(PtMountDirBO::getDir).collect(Collectors.toList()); + } + return new ArrayList<>(); + } + + public PtJupyterJobBO putFsMounts(String mountPath,String dir){ + if (StringUtils.isNotEmpty(mountPath) && StringUtils.isNotEmpty(dir)){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.TWO); + } + fsMounts.put(mountPath,new PtMountDirBO(dir)); + } + return this; + } + + public PtJupyterJobBO putFsMounts(String mountPath,PtMountDirBO dir){ + if (StringUtils.isNotEmpty(mountPath) && dir != null){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.TWO); + } + fsMounts.put(mountPath,dir); + } + return this; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterResourceBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterResourceBO.java new file mode 100644 index 0000000..3779d1f --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtJupyterResourceBO.java @@ -0,0 +1,104 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.enums.ValidationTypeEnum; + +/** + * @description Notebook BO + * @date 2020-04-17 + */ +@Data +@Accessors(chain = true) +public class PtJupyterResourceBO { + /** + * 命名空间 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + /** + * 资源名称 + **/ + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String name; + /** + * GPU数量 + **/ + private Integer gpuNum; + /** + * 是否使用gpu true:使用;false:不用 + **/ + private Boolean useGpu; + /** + * 内存数量 单位Mi Gi + **/ + private Integer memNum; + /** + * CPU数量 + **/ + private Integer cpuNum; + /** + * 镜像名称 + **/ + private String image; + + /** + * nfs存储路径,存在且能被挂载,此路径内容不能通过接口销毁,不传不会挂载 + **/ + private String datasetDir; + /** + * 本docker内的路径,挂载到datasetDir + **/ + private String datasetMountPath; + /** + * datasetDir是否只读 + **/ + private Boolean datasetReadOnly; + + /** + * nfs存储路径,存在且能被挂载,此路径内容在调用PersistentVolumeClaimApi.recycle后销毁,不传会生成默认的 + **/ + private String workspaceDir; + /** + * 本docker内的路径,挂载到datasetDir + **/ + private String workspaceMountPath; + /** + * workspaceDir的存储配额 必须 + **/ + private String workspaceRequest; + /** + * workspaceDir的存储限额 非必须 + **/ + private String workspaceLimit; + /** + * used in internal + **/ + private String workspacePvcName; + /** + * 业务标签,用于标识业务模块 + **/ + private String businessLabel; + /** + * 定时删除时间,单位:分钟 + **/ + private Integer delayDeleteTime; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtLimitRangeBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtLimitRangeBO.java new file mode 100644 index 0000000..d7a1678 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtLimitRangeBO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.domain.resource.BizLimitRangeItem; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.enums.ValidationTypeEnum; + +import java.util.List; + +/** + * @description LimitRange BO + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class PtLimitRangeBO { + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String name; + private List limits; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationDeploymentBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationDeploymentBO.java new file mode 100644 index 0000000..3321116 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationDeploymentBO.java @@ -0,0 +1,66 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; + +import java.util.List; + +/** + * @description 模型压缩 Deployment BO + * 在继承中使用 @Accessors(chain = true) 在set父类方法后会返回父类对象,故不使用 + * @date 2020-05-26 + */ +@Data +public class PtModelOptimizationDeploymentBO extends PtDeploymentBO { + /** + * 挂载到dataset的数据集的路径 + **/ + private String datasetDir; + /** + * 本docker内的路径,挂载到datasetDir,默认值/dataset + **/ + private String datasetMountPath; + /** + * 数据集是否只读 + **/ + private Boolean datasetReadOnly; + /** + * 执行命令 + **/ + private List cmdLines; + + /** + * nfs存储路径,且能被挂载,不传会生成默认的 + **/ + private String workspaceDir; + /** + * 本docker内的路径,挂载到workspaceDir,默认值默认值/workspace + **/ + private String workspaceMountPath; + + /** + * nfs存储路径,且能被挂载,默认 + **/ + private String outputDir; + /** + * 本docker内的路径,挂载到outputDir,默认值默认值/output + **/ + private String outputMountPath; + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationJobBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationJobBO.java new file mode 100644 index 0000000..8eb4610 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtModelOptimizationJobBO.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.Data; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @description 模型压缩 Job BO + * @date 2020-05-31 + */ +@Data +public class PtModelOptimizationJobBO extends PtJobBO { + /** + * 执行命令 + **/ + private List cmdLines; + + /**文件存储服务挂载 key:pod内挂载路径 value:文件存储路径及配置**/ + private Map fsMounts; + + public List getDirList(){ + if (CollectionUtil.isNotEmpty(fsMounts)){ + return fsMounts.values().stream().map(PtMountDirBO::getDir).collect(Collectors.toList()); + } + return new ArrayList<>(); + } + + public PtModelOptimizationJobBO putNfsMounts(String mountPath,String dir){ + if (StringUtils.isNotEmpty(mountPath) && StringUtils.isNotEmpty(dir)){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.TWO); + } + fsMounts.put(mountPath,new PtMountDirBO(dir)); + } + return this; + } + + public PtModelOptimizationJobBO putNfsMounts(String mountPath,PtMountDirBO dir){ + if (StringUtils.isNotEmpty(mountPath) && dir != null){ + if (fsMounts == null){ + fsMounts = new HashMap<>(MagicNumConstant.TWO); + } + fsMounts.put(mountPath,dir); + } + return this; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtMountDirBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtMountDirBO.java new file mode 100644 index 0000000..091fa8e --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtMountDirBO.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description 挂载路径参数 + * @date 2020-06-30 + */ +@Data +@AllArgsConstructor +@Accessors(chain = true) +public class PtMountDirBO { + /**挂载的路径,绝对路径**/ + private String dir; + /**是否只读 ture:是 false:否**/ + private boolean readOnly; + /**是否回收 true:创建pv、pvc进行挂载,删除时同时删除数据 false:直接挂载**/ + private boolean recycle; + /**存储配额 示例:500Mi 仅在pvc=true时生效**/ + private String request; + /**存储限额 示例:500Mi 仅在pvc=true时生效**/ + private String limit; + + public PtMountDirBO(String dir){ + this.dir = dir; + } + + public PtMountDirBO(String dir, String request){ + this.dir = dir; + this.request = request; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtPersistentVolumeClaimBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtPersistentVolumeClaimBO.java new file mode 100644 index 0000000..534bafd --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtPersistentVolumeClaimBO.java @@ -0,0 +1,111 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import cn.hutool.core.util.RandomUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.k8s.domain.resource.BizQuantity; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.enums.AccessModeEnum; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @description PVC BO + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class PtPersistentVolumeClaimBO { + private String namespace; + private String pvcName; + /** + * 指定StorageClass 的 provisioner + **/ + private String provisioner; + /** + * 指定StorageClass 的 volumeBindingMode + **/ + private String volumeBindingMode; + /** + * 资源配额 + **/ + private Map capacity; + /** + * PVC的accessModes ReadWriteOnce:单node的读写 ReadOnlyMany:多node的只读 ReadWriteMany:多node的读写 + **/ + private Set accessModes; + /** + * 标签 + **/ + private Map labels; + /** + * 用户填写的资源名称 + **/ + private String resourceName; + /** + * 关于provisioner的配置,目前用不到 + **/ + private Map parameters; + /** + * 存储限额 + **/ + private String limit; + /** + * 存储配额 + **/ + private String request; + /** + * pv挂载存储路径 + **/ + private String path; + + public PtPersistentVolumeClaimBO() { + + } + + public PtPersistentVolumeClaimBO(PtJupyterResourceBO bo) { + this.labels = new HashMap<>(); + this.namespace = bo.getNamespace(); + this.request = bo.getWorkspaceRequest(); + this.limit = bo.getWorkspaceLimit(); + this.path = bo.getWorkspaceDir(); + this.resourceName = bo.getName(); + this.accessModes = new HashSet() {{ + add(AccessModeEnum.READ_WRITE_ONCE.getType()); + }}; + this.setPvcName(bo.getName() + "-" + RandomUtil.randomString(MagicNumConstant.FIVE)); + } + + public PtPersistentVolumeClaimBO(String namespace,String resourceName,PtMountDirBO bo){ + this.labels = new HashMap<>(); + this.namespace = namespace; + this.request = bo.getRequest(); + this.limit = bo.getLimit(); + this.path = bo.getDir(); + this.resourceName = resourceName; + this.accessModes = new HashSet(){{ + add(AccessModeEnum.READ_WRITE_ONCE.getType()); + }}; + this.setPvcName(resourceName+"-"+RandomUtil.randomString(MagicNumConstant.FIVE)); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtResourceQuotaBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtResourceQuotaBO.java new file mode 100644 index 0000000..e423f75 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/PtResourceQuotaBO.java @@ -0,0 +1,81 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.k8s.annotation.K8sValidation; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.resource.BizQuantity; +import org.dubhe.k8s.domain.resource.BizScopedResourceSelectorRequirement; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.enums.ValidationTypeEnum; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @description ResourceQuota BO + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class PtResourceQuotaBO { + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String namespace; + @K8sValidation(ValidationTypeEnum.K8S_RESOURCE_NAME) + private String name; + private Map hard; + private List scopeSelector; + + /** + * 添加cpu 限制 + * @param amount 值 + * @param format 单位 + */ + public void addCpuLimitsHard(String amount,String format){ + if (hard == null){ + hard = new HashMap<>(); + } + hard.put(K8sParamConstants.RESOURCE_QUOTA_CPU_LIMITS_KEY,new BizQuantity(amount,format)); + } + + /** + * 添加cpu 限制 + * @param amount 值 + * @param format 单位 + */ + public void addMemoryLimitsHard(String amount,String format){ + if (hard == null){ + hard = new HashMap<>(); + } + hard.put(K8sParamConstants.RESOURCE_QUOTA_MEMORY_LIMITS_KEY,new BizQuantity(amount,format)); + } + + /** + * 添加gpu 限制 + * @param amount 值 + */ + public void addGpuLimitsHard(String amount){ + if (hard == null){ + hard = new HashMap<>(); + } + hard.put(K8sParamConstants.RESOURCE_QUOTA_GPU_LIMITS_KEY,new BizQuantity(amount, SymbolConstant.BLANK)); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ResourceYamlBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ResourceYamlBO.java new file mode 100644 index 0000000..7e5f987 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/ResourceYamlBO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import lombok.Data; + +/** + * @description K8s resource yaml description + * @date 2020-08-28 + */ +@Data +public class ResourceYamlBO { + /** + * K8s resource kind + */ + private String kind; + /** + * K8s resource yaml definition + */ + private String yaml; + + public ResourceYamlBO(String kind,String yaml){ + this.kind = kind; + this.yaml = yaml; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/TaskYamlBO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/TaskYamlBO.java new file mode 100644 index 0000000..c2c31ff --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/bo/TaskYamlBO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.bo; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import lombok.Data; +import org.dubhe.k8s.utils.YamlUtils; + +import java.util.LinkedList; +import java.util.List; + +/** + * @description 任务k8s资源列表 + * @date 2020-08-28 + */ +@Data +public class TaskYamlBO { + private List yamlList; + + public TaskYamlBO() { + yamlList = new LinkedList(); + } + + public void append(HasMetadata resource) { + String kind = resource.getKind(); + yamlList.add(new ResourceYamlBO(kind, YamlUtils.dumpAsYaml(resource))); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrain.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrain.java new file mode 100644 index 0000000..651200e --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrain.java @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.domain.cr; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.CustomResource; + +/** + * @description 自定义资源 DistributeTrain + * @date 2020-07-07 + */ +public class DistributeTrain extends CustomResource implements Namespaced { + private String apiVersion = "onebrain.oneflow.org/v1alpha1"; + private String kind = "DistributeTrain"; + + private DistributeTrainSpec spec; + + @Override + public String toString() { + return "DistributeTrain{" + + "apiVersion='" + getApiVersion() + '\'' + + ", metadata=" + getMetadata() + + ", spec=" + spec + + '}'; + } + + public DistributeTrainSpec getSpec() { + return spec; + } + + public void setSpec(DistributeTrainSpec spec) { + this.spec = spec; + } + + @Override + public ObjectMeta getMetadata() { return super.getMetadata(); } + + @Override + public String getApiVersion() { + return apiVersion; + } + + @Override + public String getKind() { + return kind; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainDoneable.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainDoneable.java new file mode 100644 index 0000000..4f0dcc1 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainDoneable.java @@ -0,0 +1,30 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.domain.cr; + +import io.fabric8.kubernetes.api.builder.Function; +import io.fabric8.kubernetes.client.CustomResourceDoneable; + +/** + * @description 自定义资源的 DistributeTrainDoneable + * @date 2020-07-07 + */ +public class DistributeTrainDoneable extends CustomResourceDoneable { + public DistributeTrainDoneable(DistributeTrain resource, Function function) { + super(resource, function); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainList.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainList.java new file mode 100644 index 0000000..0171c7a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainList.java @@ -0,0 +1,26 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.domain.cr; + +import io.fabric8.kubernetes.client.CustomResourceList; + +/** + * @description 自定义资源dt集合类 + * @date 2020-07-07 + */ +public class DistributeTrainList extends CustomResourceList { +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainSpec.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainSpec.java new file mode 100644 index 0000000..5bc6d3d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/cr/DistributeTrainSpec.java @@ -0,0 +1,175 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.domain.cr; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.fabric8.kubernetes.api.model.*; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @description 自定义资源dt详情类 + * @date 2020-07-07 + */ +@JsonDeserialize( + using = JsonDeserializer.None.class +) +public class DistributeTrainSpec implements KubernetesResource { + private Integer size; + private String image; + private String imagePullPolicy; + private String masterCmd; + private ResourceRequirements masterResources; + private String slaveCmd; + private ResourceRequirements slaveResources; + private Map nodeSelector = new HashMap<>(); + private List env = new ArrayList<>(); + /** + * 内部映射 + */ + private List volumeMounts; + /** + * 外部挂载 + */ + private List volumes; + + /** + * 容忍度 + */ + private List tolerations = new ArrayList<>(); + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public String getImagePullPolicy() { + return imagePullPolicy; + } + + public void setImagePullPolicy(String imagePullPolicy) { + this.imagePullPolicy = imagePullPolicy; + } + + public String getMasterCmd() { + return masterCmd; + } + + public void setMasterCmd(String masterCmd) { + this.masterCmd = masterCmd; + } + + public ResourceRequirements getMasterResources() { + return masterResources; + } + + public void setMasterResources(ResourceRequirements masterResources) { + this.masterResources = masterResources; + } + + public String getSlaveCmd() { + return slaveCmd; + } + + public void setSlaveCmd(String slaveCmd) { + this.slaveCmd = slaveCmd; + } + + public ResourceRequirements getSlaveResources() { + return slaveResources; + } + + public void setSlaveResources(ResourceRequirements slaveResources) { + this.slaveResources = slaveResources; + } + + public Map getNodeSelector() { + return nodeSelector; + } + + public void setNodeSelector(Map nodeSelector) { + this.nodeSelector = nodeSelector; + } + + public void addNodeSelector(Map nodeSelector){ + if (CollectionUtils.isEmpty(nodeSelector)){ + return; + } + if (this.nodeSelector == null){ + this.nodeSelector = nodeSelector; + } + this.nodeSelector.putAll(nodeSelector); + } + + public List getEnv() { + return env; + } + + public void setEnv(List env) { + this.env = env; + } + + public List getVolumeMounts() { + return volumeMounts; + } + + public void setVolumeMounts(List volumeMounts) { + this.volumeMounts = volumeMounts; + } + + public List getVolumes() { + return volumes; + } + + public void setVolumes(List volumes) { + this.volumes = volumes; + } + + public List getTolerations() { + return tolerations; + } + + public void setTolerations(List tolerations) { + this.tolerations = tolerations; + } + + public void addTolerations(List tolerations){ + if (CollectionUtils.isEmpty(tolerations)){ + return ; + } + if (this.tolerations == null){ + this.tolerations = tolerations; + } + this.tolerations.addAll(tolerations); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sDeploymentCallbackCreateDTO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sDeploymentCallbackCreateDTO.java new file mode 100644 index 0000000..bc779c8 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sDeploymentCallbackCreateDTO.java @@ -0,0 +1,82 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @descripton 统一通用参数实现与校验 + * @date 2020-11-26 + */ +@ApiModel(description = "k8s deployment异步回调基类") +@Data +public class BaseK8sDeploymentCallbackCreateDTO { + @ApiModelProperty(required = true, value = "k8s namespace") + @NotBlank(message = "namespace 不能为空!") + private String namespace; + + @ApiModelProperty(required = true, value = "k8s resource name") + @NotBlank(message = "resourceName 不能为空!") + private String resourceName; + + @ApiModelProperty(required = true, value = "k8s deployment name") + @NotBlank(message = "deployment 不能为空!") + private String deploymentName; + + /** + * deployment已 Running的pod数 + */ + @ApiModelProperty(required = true, value = "k8s deployment readyReplicas") + @NotNull(message = "readyReplicas 不能为空!") + private Integer readyReplicas; + + /** + * deployment总pod数 + */ + @ApiModelProperty(required = true, value = "k8s deployment replicas") + @NotNull(message = "replicas 不能为空!") + private Integer replicas; + + public BaseK8sDeploymentCallbackCreateDTO() { + + } + + public BaseK8sDeploymentCallbackCreateDTO(String namespace, String resourceName, String deploymentName, Integer readyReplicas, Integer replicas) { + this.namespace = namespace; + this.resourceName = resourceName; + this.deploymentName = deploymentName; + this.readyReplicas = readyReplicas; + this.replicas = replicas; + } + + @Override + public String toString() { + return "BaseK8sDeploymentCallbackCreateDTO{" + + "namespace='" + namespace + '\'' + + ", resourceName='" + resourceName + '\'' + + ", deploymentName='" + deploymentName + '\'' + + ", readyReplicas='" + readyReplicas + '\'' + + ", replicas='" + replicas + '\'' + + '}'; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sPodCallbackCreateDTO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sPodCallbackCreateDTO.java new file mode 100644 index 0000000..c283837 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/BaseK8sPodCallbackCreateDTO.java @@ -0,0 +1,87 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * @descripton 统一通用参数实现与校验 + * @date 2020-05-28 + */ +@ApiModel(description = "k8s pod异步回调基类") +@Data +public class BaseK8sPodCallbackCreateDTO { + + @ApiModelProperty(required = true,value = "k8s namespace") + @NotEmpty(message = "namespace 不能为空!") + private String namespace; + + @ApiModelProperty(required = true,value = "k8s resource name") + @NotEmpty(message = "resourceName 不能为空!") + private String resourceName; + + @ApiModelProperty(required = true,value = "k8s pod name") + @NotEmpty(message = "podName 不能为空!") + private String podName; + + @ApiModelProperty(required = true,value = "k8s pod parent type") + @NotEmpty(message = "podParentType 不能为空!") + private String podParentType; + + @ApiModelProperty(required = true,value = "k8s pod parent name") + @NotEmpty(message = "podParentName 不能为空!") + private String podParentName; + + @ApiModelProperty(value = "k8s pod phase",notes = "对应PodPhaseEnum") + @NotEmpty(message = "phase 不能为空!") + private String phase; + + @ApiModelProperty(value = "k8s pod containerStatuses state") + private String messages; + + public BaseK8sPodCallbackCreateDTO(){ + + } + + public BaseK8sPodCallbackCreateDTO(String namespace, String resourceName, String podName, String podParentType, String podParentName, String phase, String messages){ + this.namespace = namespace; + this.resourceName = resourceName; + this.podName = podName; + this.podParentType = podParentType; + this.podParentName = podParentName; + this.phase = phase; + this.messages = messages; + } + + @Override + public String toString() { + return "BaseK8sPodCallbackReq{" + + "namespace='" + namespace + '\'' + + ", resourceName='" + resourceName + '\'' + + ", podName='" + podName + '\'' + + ", podParentType='" + podParentType + '\'' + + ", podParentName='" + podParentName + '\'' + + ", phase='" + phase + '\'' + + ", messages=" + messages + + '}'; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/NodeIsolationDTO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/NodeIsolationDTO.java new file mode 100644 index 0000000..d900724 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/NodeIsolationDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.dto; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** + * @description k8s节点资源隔离DTO + * @date 2021-05-17 + */ +@Data +@Api("k8s节点资源隔离DTO") +public class NodeIsolationDTO { + @ApiModelProperty("节点名称列表") + @NotEmpty(message = "节点名称为空") + private List nodeNames; + + @ApiModelProperty("用户id") + private Long userId; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogDownloadQueryDTO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogDownloadQueryDTO.java new file mode 100644 index 0000000..cb2b895 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogDownloadQueryDTO.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.dto; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.domain.vo.PodVO; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** + * @description Pod日志 下载DTO + * @date 2020-08-21 + */ +@Data +@Accessors(chain = true) +@Api("Pod日志 下载DTO") +public class PodLogDownloadQueryDTO { + + @ApiModelProperty("k8s节点信息") + @NotEmpty(message = "k8s节点信息为空") + private List podVOList; + + @ApiModelProperty(value = "命名空间", required = true) + @NotBlank(message = "命名空间不能为空") + private String namespace; + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogQueryDTO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogQueryDTO.java new file mode 100644 index 0000000..52d8d2d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodLogQueryDTO.java @@ -0,0 +1,97 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description Pod日志 查询DTO + * @date 2020-08-14 + */ +@Data +@Accessors(chain = true) +@Api("Pod日志 查询DTO") +public class PodLogQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "命名空间", required = true) + @NotBlank(message = "命名空间不能为空") + private String namespace; + + @ApiModelProperty("k8s实际pod名称") + @NotNull(message = "pod名称不能为空") + private String podName; + + @ApiModelProperty(value = "起始行") + private Integer startLine; + + @ApiModelProperty(value = "查询行数") + private Integer lines; + + @ApiModelProperty(value = "日志查询条件:关键字") + private String logKeyword; + + @ApiModelProperty(value = "日志查询时间范围:开始时间") + private Long beginTimeMillis; + + @ApiModelProperty(value = "日志查询时间范围:结束时间") + private Long endTimeMillis; + + public PodLogQueryDTO() { + + } + + public PodLogQueryDTO(String podName) { + this.podName = podName; + } + + /** + * 初始化获取起始行 + * @return + */ + @JsonIgnore + public int getQueryStart() { + if (startLine == null || startLine < MagicNumConstant.ONE) { + return MagicNumConstant.ONE; + } + return startLine; + } + + /** + * 初始化获取查询行数 + * @return + */ + @JsonIgnore + public int getQueryLines() { + if (lines == null || lines < MagicNumConstant.ONE || lines > MagicNumConstant.FOUR_THOUSAND) { + return MagicNumConstant.FOUR_THOUSAND; + } + return lines; + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodQueryDTO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodQueryDTO.java new file mode 100644 index 0000000..127fb6a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/dto/PodQueryDTO.java @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.domain.dto; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.MagicNumConstant; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * @description Pod基础信息查询入参 + * @date 2020-08-14 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Api("Pod基础信息查询入参") +public class PodQueryDTO { + + @ApiModelProperty(value = "命名空间", required = true) + @NotBlank(message = "命名空间不能为空") + private String namespace; + + @ApiModelProperty(value = "资源名称", required = false) + private String resourceName; + + @ApiModelProperty(value = "pod名称列表", required = false) + private List podNames; + + @ApiModelProperty(value = "开始时间(unix时间戳/秒)", required = false) + private Long startTime; + + @ApiModelProperty(value = "结束时间(unix时间戳/秒)", required = false) + private Long endTime; + + @ApiModelProperty(value = "步长/秒", required = false) + private Integer step; + + /** + * 4小时秒数 + */ + private static final Long FOUR_HOUR_SECONDS = MagicNumConstant.SIXTY_LONG * MagicNumConstant.SIXTY_LONG * MagicNumConstant.FOUR; + + public PodQueryDTO(String namespace, String resourceName) { + this.namespace = namespace; + this.resourceName = resourceName; + } + + /** + * 未设置部分参数时生成默认参数 + */ + public void generateDefaultParam() { + if (startTime == null) { + startTime = System.currentTimeMillis() / MagicNumConstant.THOUSAND_LONG - FOUR_HOUR_SECONDS; + } + if (endTime == null) { + endTime = System.currentTimeMillis() / MagicNumConstant.THOUSAND_LONG; + } + if (step == null || step == 0) { + step = MagicNumConstant.TEN; + } + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sResource.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sResource.java new file mode 100644 index 0000000..4d70dbe --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sResource.java @@ -0,0 +1,65 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.biz.db.entity.BaseEntity; + +/** + * @description k8s资源对象 + * @date 2020-07-10 + */ +@Data +@TableName("k8s_resource") +public class K8sResource extends BaseEntity { + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty(hidden = true) + private Long id; + + @TableField(value = "kind") + private String kind; + + @TableField(value = "namespace") + private String namespace; + + @TableField(value = "name") + private String name; + + @TableField(value = "resource_name") + private String resourceName; + + @TableField(value = "env") + private String env; + + @TableField(value = "business") + private String business; + + public K8sResource(String kind,String namespace,String name,String resourceName,String env,String business){ + this.kind = kind; + this.namespace = namespace; + this.name = name; + this.resourceName = resourceName; + this.env = env; + this.business = business; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sTask.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sTask.java new file mode 100644 index 0000000..e9d22ee --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/entity/K8sTask.java @@ -0,0 +1,110 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.entity; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.db.entity.BaseEntity; +import org.dubhe.k8s.domain.bo.TaskYamlBO; +import org.dubhe.k8s.enums.K8sTaskStatusEnum; +import org.dubhe.biz.base.utils.StringUtils; + +import java.sql.Timestamp; + +/** + * @description k8s任务对象 + * @date 2020-8-31 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("k8s_task") +public class K8sTask extends BaseEntity{ + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty(hidden = true) + private Long id; + + @TableField(value = "namespace") + private String namespace; + + @TableField(value = "resource_name") + private String resourceName; + + @TableField(value = "task_yaml") + private String taskYaml; + + @TableField(value = "business") + private String business; + + @TableField(value = "apply_unix_time") + private Long applyUnixTime; + + @TableField(value = "apply_display_time") + private Timestamp applyDisplayTime; + + @TableField(value = "apply_status") + private Integer applyStatus; + + @TableField(value = "stop_unix_time") + private Long stopUnixTime; + + @TableField(value = "stop_display_time") + private Timestamp stopDisplayTime; + + @TableField(value = "stop_status") + private Integer stopStatus; + + public TaskYamlBO getTaskYamlBO(){ + if (StringUtils.isEmpty(taskYaml)){ + return null; + } + return JSON.parseObject(taskYaml, TaskYamlBO.class); + } + + public void setTaskYamlBO(TaskYamlBO taskYamlBO){ + taskYaml = JSON.toJSONString(taskYamlBO); + } + + public boolean needCreate(Long time){ + boolean needCreate = applyUnixTime < time && K8sTaskStatusEnum.UNEXECUTED.getStatus().equals(applyStatus); + boolean needDelete = stopUnixTime < time && K8sTaskStatusEnum.UNEXECUTED.getStatus().equals(stopStatus); + return needCreate && (needCreate ^ needDelete); + } + + public boolean needDelete(Long time){ + boolean needCreate = applyUnixTime < time && K8sTaskStatusEnum.UNEXECUTED.getStatus().equals(applyStatus); + boolean needDelete = stopUnixTime < time && K8sTaskStatusEnum.UNEXECUTED.getStatus().equals(stopStatus); + return needDelete && (needCreate ^ needDelete); + } + + /** + * 判断任务是否已超时 + * @param time + * @return + */ + public boolean overtime(Long time){ + return applyUnixTime < time && K8sTaskStatusEnum.UNEXECUTED.getStatus().equals(applyStatus) && stopUnixTime < time && K8sTaskStatusEnum.UNEXECUTED.getStatus().equals(stopStatus); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainer.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainer.java new file mode 100644 index 0000000..c7482ec --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainer.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +import java.util.List; +import java.util.Map; + +/** + * @description BizContainer对象 + * @date 2020-04-15 + */ +@Data +@Accessors(chain = true) +public class BizContainer { + @K8sField("image") + private String image; + @K8sField("imagePullPolicy") + private String imagePullPolicy; + @K8sField("name") + private String name; + @K8sField("volumeMounts") + private List volumeMounts; + @K8sField("resources:limits") + private Map limits; + @K8sField("resources:requests") + private Map requests; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateTerminated.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateTerminated.java new file mode 100644 index 0000000..f3e6002 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateTerminated.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description BizContainerStateTerminated实体类 + * @date 2020-06-02 + */ +@Data +@Accessors(chain = true) +public class BizContainerStateTerminated { + @K8sField("finishedAt") + private String finishedAt; + @K8sField("message") + private String message; + @K8sField("reason") + private String reason; + @K8sField("startedAt") + private String startedAt; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateWaiting.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateWaiting.java new file mode 100644 index 0000000..347affd --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStateWaiting.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description BizContainerStateWaiting实体 + * @date 2020-06-02 + */ +@Data +@Accessors(chain = true) +public class BizContainerStateWaiting { + @K8sField("message") + private String message; + @K8sField("reason") + private String reason; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStatus.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStatus.java new file mode 100644 index 0000000..8beaa63 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizContainerStatus.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description BizContainerStatus实体类 + * @date 2020-06-02 + */ +@Data +@Accessors(chain = true) +public class BizContainerStatus { + /** + * Details about a terminated container + */ + @K8sField("state:terminated") + private BizContainerStateTerminated terminated; + /** + * Details about a waiting container + */ + @K8sField("state:waiting") + private BizContainerStateWaiting waiting; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeployment.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeployment.java new file mode 100644 index 0000000..d787aa1 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeployment.java @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.domain.PtBaseResult; + +import java.util.List; +import java.util.Map; + +/** + * @description Deployment 业务类 + * @date 2020-05-28 + */ +@Data +@Accessors(chain = true) +public class BizDeployment extends PtBaseResult { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + @K8sField("metadata:name") + private String name; + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("metadata:resourceVersion") + private String resourceVersion; + + @K8sField("spec:template:spec:containers") + private List containers; + + @K8sField("status:conditions") + private List conditions; + + @K8sField("status:replicas") + private Integer replicas; + + @K8sField("status:readyReplicas") + private Integer readyReplicas; + + /** + * 获取业务标签 + * @return + */ + public String getBusinessLabel() { + return labels.get(K8sLabelConstants.BASE_TAG_BUSINESS); + } + + /** + * 根据键获取label + * + * @param labelKey + * @return + */ + public String getLabel(String labelKey) { + return labels.get(labelKey); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeploymentCondition.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeploymentCondition.java new file mode 100644 index 0000000..ecd320d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDeploymentCondition.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description BizDeploymentCondition实体类 + * @date 2020-05-28 + */ +@Data +@Accessors(chain = true) +public class BizDeploymentCondition { + @K8sField("lastTransitionTime") + private String lastTransitionTime; + @K8sField("message") + private java.lang.String message; + @K8sField("reason") + private java.lang.String reason; + @K8sField("status") + private String status; + @K8sField("type") + private String type; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrain.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrain.java new file mode 100644 index 0000000..e3e6ca7 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrain.java @@ -0,0 +1,66 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import com.google.common.collect.Maps; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeMount; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; + +import java.util.List; +import java.util.Map; + +/** + * @description BizDistributeTrain实体类 + * @date 2020-07-08 + */ +@Data +@Accessors(chain = true) +public class BizDistributeTrain extends PtBaseResult { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + @K8sField("metadata:name") + private String name; + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + @K8sField("metadata:namespace") + private String namespace; + @K8sField("spec:size") + private Integer size; + @K8sField("spec:image") + private String image; + @K8sField("spec:masterCmd") + private String masterCmd; + @K8sField("spec:masterResources") + private BizDistributeTrainResources masterResources; + @K8sField("spec:slaveCmd") + private String slaveCmd; + @K8sField("spec:slaveResources") + private BizDistributeTrainResources slaveResources; + @K8sField("spec:nodeSelector") + private Map nodeSelector = Maps.newHashMap(); + @K8sField("spec:volumeMounts") + private List volumeMounts; + @K8sField("spec:volumes") + private List volumes; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainContainer.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainContainer.java new file mode 100644 index 0000000..6cef96f --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainContainer.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description BizDistributeTrainContainer实体类 + * @date 2020-07-08 + */ +@Data +@Accessors(chain = true) +public class BizDistributeTrainContainer { + @K8sField("name") + private String name; + @K8sField("image") + private String image; + @K8sField("imagePullPolicy") + private String imagePullPolicy; + @K8sField("volumeMounts") + private List volumeMounts; + @K8sField("command") + private List command = new ArrayList<>(); + @K8sField("args") + private List args = new ArrayList<>(); + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainResources.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainResources.java new file mode 100644 index 0000000..2976805 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizDistributeTrainResources.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +import java.util.Map; + +/** + * @description BizDistributeTrainResources实体类 + * @date 2020-07-08 + */ +@Data +@Accessors(chain = true) +public class BizDistributeTrainResources { + @K8sField("limits") + private Map limits; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizHTTPIngressPath.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizHTTPIngressPath.java new file mode 100644 index 0000000..7fe38b1 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizHTTPIngressPath.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description Kubernetes HTTPIngressPath + * @date 2020-09-17 + */ +@Data +@Accessors(chain = true) +public class BizHTTPIngressPath { + @K8sField("backend:serviceName") + private String serviceName; + @K8sField("backend:servicePort") + private BizIntOrString servicePort; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngress.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngress.java new file mode 100644 index 0000000..92c7fd9 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngress.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +import java.util.List; +import java.util.Map; + +/** + * @description Kubernetes Ingress + * @date 2020-09-09 + */ +@Data +@Accessors(chain = true) +public class BizIngress { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + @K8sField("metadata:name") + private String name; + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("metadata:resourceVersion") + private String resourceVersion; + + @K8sField("spec:rules") + private List rules; + + @K8sField("spec:tls") + private List tls; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressRule.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressRule.java new file mode 100644 index 0000000..102ebc8 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressRule.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +import java.util.List; + +/** + * @description Kubernetes IngressRule + * @date 2020-09-09 + */ +@Data +@Accessors(chain = true) +public class BizIngressRule { + @K8sField("host") + private String host; + @K8sField("http:paths") + private List paths; + + private String servicePort; + + /** + * 获取service 端口 + */ + public void takeServicePort(){ + if (paths == null || paths.size() == 0){ + return; + } + servicePort = paths.get(0).getServicePort().getStrVal() == null ? String.valueOf(paths.get(0).getServicePort().getIntVal()):paths.get(0).getServicePort().getStrVal(); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressTLS.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressTLS.java new file mode 100644 index 0000000..e2247fa --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIngressTLS.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +import java.util.List; + +/** + * @description Kubernetes IngressTLS + * @date 2020-09-10 + */ +@Data +@Accessors(chain = true) +public class BizIngressTLS { + @K8sField("hosts") + private List hosts; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIntOrString.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIntOrString.java new file mode 100644 index 0000000..2403190 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizIntOrString.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description Kubernetes BizIntOrString + * @date 2020-09-17 + */ +@Data +@Accessors(chain = true) +public class BizIntOrString { + @K8sField("IntVal") + private Integer IntVal; + @K8sField("Kind") + private Integer Kind; + @K8sField("StrVal") + private String StrVal; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJob.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJob.java new file mode 100644 index 0000000..8594333 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJob.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.domain.PtBaseResult; + +import java.util.List; +import java.util.Map; + +/** + * @description BizJob实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizJob extends PtBaseResult { + @K8sField("metadata:name") + private String name; + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("spec:template:spec:containers") + private List containers; + @K8sField("status:completionTime") + private String completionTime; + @K8sField("status:conditions") + private List conditions; + @K8sField("status:startTime") + private String startTime; + @K8sField("status:succeeded") + private Integer succeeded; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJobCondition.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJobCondition.java new file mode 100644 index 0000000..48ed15a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizJobCondition.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizJobCondition实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizJobCondition { + @K8sField("lastProbeTime") + private String lastProbeTime; + @K8sField("lastTransitionTime") + private String lastTransitionTime; + @K8sField("status") + private String status; + /** + * PodScheduled:已将Pod调度到一个节点; + * Ready:该Pod能够处理请求,应将其添加到所有匹配服务的负载平衡池中; + * Initialized:所有初始化容器已成功启动; + * ContainersReady:容器中的所有容器均已准备就绪。 + */ + @K8sField("type") + private String type; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRange.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRange.java new file mode 100644 index 0000000..e5a5ae7 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRange.java @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * @description BizLimitRange实体类 + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class BizLimitRange extends PtBaseResult { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("metadata:resourceVersion") + private String resourceVersion; + + @K8sField("spec:limits") + private List limits; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRangeItem.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRangeItem.java new file mode 100644 index 0000000..3f10a9d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizLimitRangeItem.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Map; + +/** + * @description BizLimitRangeItem实体类 + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class BizLimitRangeItem extends PtBaseResult { + @K8sField("default") + Map _default; + @K8sField("defaultRequest") + Map defaultRequest; + @K8sField("max") + Map max; + @K8sField("maxLimitRequestRatio") + Map maxLimitRequestRatio; + @K8sField("min") + Map min; + @K8sField("type") + private String type = "Container"; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNamespace.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNamespace.java new file mode 100644 index 0000000..41718d2 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNamespace.java @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Map; + +/** + * @description Namespace 业务类 + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class BizNamespace extends PtBaseResult { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + @K8sField("metadata:name") + private String name; + @K8sField("metadata:uid") + private String uid; + @K8sField("metadata:resourceVersion") + private String resourceVersion; + + @K8sField("status:phase") + private String phase; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNode.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNode.java new file mode 100644 index 0000000..e558db3 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNode.java @@ -0,0 +1,111 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import cn.hutool.core.collection.CollectionUtil; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.enums.NodeConditionTypeEnum; + +import java.util.List; +import java.util.Map; + +/** + * @description BizNode实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizNode extends PtBaseResult { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + /** + * node的标签 + */ + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + /** + * node的名称 + */ + @K8sField("metadata:name") + private String name; + /** + * 唯一标识 + */ + @K8sField("metadata:uid") + private String uid; + @K8sField("metadata:resourceVersion") + private String resourceVersion; + /** + * 不可调度,为true时pod不会调度到此节点 + */ + @K8sField("spec:unschedulable") + private boolean unschedulable; + /** + * 污点 + */ + @K8sField("spec:taints") + private List taints; + + /** + * 节点可到达的地址列表,主机名和ip + */ + @K8sField("status:addresses") + private List addresses; + /** + * 可用于调度的节点资源 + */ + @K8sField("status:allocatable") + private Map allocatable; + /** + * 节点的总资源 + */ + @K8sField("status:capacity") + private Map capacity; + /** + * 当前的节点状态数组 + */ + @K8sField("status:conditions") + private List conditions; + /** + * 节点的一些信息 + */ + @K8sField("status:nodeInfo") + private BizNodeSystemInfo nodeInfo; + + /** + * 是否ready + */ + private Boolean ready; + + public BizNode setReady() { + if (CollectionUtil.isNotEmpty(conditions)) { + ready = conditions.stream().filter(obj -> NodeConditionTypeEnum.READY.getType().equals(obj.getType()) && K8sParamConstants.NODE_READY_TRUE.equals(obj.getStatus())).count() > 0; + } + return this; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeAddress.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeAddress.java new file mode 100644 index 0000000..b44059b --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeAddress.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizNodeAddress实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizNodeAddress { + @K8sField("address") + private String address; + @K8sField("type") + private String type; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeCondition.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeCondition.java new file mode 100644 index 0000000..defcf78 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeCondition.java @@ -0,0 +1,65 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizNodeCondition实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizNodeCondition { + /** + * 上次心跳时间 + */ + @K8sField("lastHeartbeatTime") + private String lastHeartbeatTime; + /** + * 上一次从一种状态转换为另一种状态的时间戳 + */ + @K8sField("lastTransitionTime") + private String lastTransitionTime; + /** + * 相关信息 + */ + @K8sField("message") + private String message; + /** + * 原因 + */ + @K8sField("reason") + private String reason; + /** + * True or False + */ + @K8sField("status") + private String status; + /** + * NetworkUnavailable:网络是否可用 + * MemoryPressure:内存压力 + * DiskPressure:磁盘压力 + * PIDPressure:PID压力 + * KubeletReady:node是否准备好 + */ + @K8sField("type") + private String type; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeSystemInfo.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeSystemInfo.java new file mode 100644 index 0000000..ec67f19 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizNodeSystemInfo.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizNodeSystemInfo实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizNodeSystemInfo { + @K8sField("architecture") + private String architecture; + @K8sField("bootID") + private String bootID; + @K8sField("containerRuntimeVersion") + private String containerRuntimeVersion; + @K8sField("kernelVersion") + private String kernelVersion; + @K8sField("kubeProxyVersion") + private String kubeProxyVersion; + @K8sField("kubeletVersion") + private String kubeletVersion; + @K8sField("machineID") + private String machineID; + @K8sField("operatingSystem") + private String operatingSystem; + @K8sField("osImage") + private String osImage; + @K8sField("systemUUID") + private String systemUUID; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaim.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaim.java new file mode 100644 index 0000000..32cb16d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaim.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; +import java.util.Map; + +/** + * @description BizPersistentVolumeClaim实体类 + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class BizPersistentVolumeClaim extends PtBaseResult { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("metadata:resourceVersion") + private String resourceVersion; + + @K8sField("spec:accessModes") + private List accessModes; + @K8sField("spec:resources:limits") + private Map limits; + @K8sField("spec:resources:requests") + private Map requests; + @K8sField("spec:storageClassName") + private String storageClassName; + @K8sField("spec:volumeMode") + private String volumeMode; + + @K8sField("status:capacity") + private Map capacity; + @K8sField("status:conditions") + private List conditions; + @K8sField("status:phase") + private String phase; + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaimCondition.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaimCondition.java new file mode 100644 index 0000000..a7b6ac3 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPersistentVolumeClaimCondition.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizPersistentVolumeClaimCondition实体类 + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class BizPersistentVolumeClaimCondition extends PtBaseResult { + @K8sField("lastProbeTime") + private String lastProbeTime; + @K8sField("lastTransitionTime") + private String lastTransitionTime; + @K8sField("message") + private String message; + @K8sField("reason") + private String reason; + @K8sField("status") + private String status; + @K8sField("type") + private String type; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPod.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPod.java new file mode 100644 index 0000000..80bcbdd --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPod.java @@ -0,0 +1,128 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import cn.hutool.core.util.StrUtil; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.constant.K8sLabelConstants; + +import java.util.List; +import java.util.Map; + + +/** + * @description 供业务层使用的k8s pod + * @date 2020-04-15 + */ +@Data +@Accessors(chain = true) +public class BizPod extends PtBaseResult { + private static final String CONTAINER_STATE_MESSAGE = "Pod {} {} reason : {}, message {} "; + + @K8sField("metadata:name") + private String name; + /** + * 创建此资源对象时间戳 + */ + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + @K8sField("metadata:labels") + private Map labels = Maps.newHashMap(); + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("spec:containers") + private List containers; + @K8sField("spec:nodeName") + private String nodeName; + @K8sField("status:podIP") + private String podIp; + @K8sField("spec:volumes") + private List volumes; + + /** + * Pending:待处理 + * Running:运行 + * Succeeded:pod中的所有container已成功终止,将不会重新启动 + * Failed:pod中的所有container均已终止,并且至少一个容器因故障而终止 + * Unknown:由于某种原因,无法获得Pod的状态 + */ + @K8sField("status:phase") + private String phase; + /** + * Kubelet确认时间,此时间戳生成在pull images之前 + */ + @K8sField("status:startTime") + private String startTime; + /** + * 状态变化时间戳 + */ + @K8sField("status:conditions") + private List conditions; + /** + * container 状态 + */ + @K8sField("status:containerStatuses") + private List containerStatuses; + + /** + * 训练结束时间 + */ + private String completedTime; + + public String getBusinessLabel() { + return labels.get(K8sLabelConstants.BASE_TAG_BUSINESS); + } + + /** + * 根据键获取label + * + * @param labelKey + * @return + */ + public String getLabel(String labelKey) { + return labels.get(labelKey); + } + + /** + * 拼接message + * + * @return + */ + public String getContainerStateMessages() { + StringBuilder messages = new StringBuilder(); + if (containerStatuses == null) { + return null; + } + containerStatuses.stream().map(obj -> { + if (obj.getTerminated() != null) { + messages.append(StrUtil.format(CONTAINER_STATE_MESSAGE, name, phase, obj.getTerminated().getReason(), obj.getTerminated().getMessage())); + } + if (obj.getWaiting() != null) { + messages.append(StrUtil.format(CONTAINER_STATE_MESSAGE, name, phase, obj.getWaiting().getReason(), obj.getWaiting().getMessage())); + } + return null; + }); + return messages.toString(); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodCondition.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodCondition.java new file mode 100644 index 0000000..629422c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodCondition.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizPodCondition实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizPodCondition { + /** + * Pod上一次从一种状态转换为另一种状态的时间戳 + */ + @K8sField("lastTransitionTime") + private String lastTransitionTime; + /** + * Pod状态信息 + */ + @K8sField("message") + private String message; + /** + * True or False + */ + @K8sField("status") + private String status; + /** + * PodScheduled:已将Pod调度到一个节点; + * Ready:该Pod能够处理请求,应将其添加到所有匹配服务的负载平衡池中; + * Initialized:所有初始化容器已成功启动; + * ContainersReady:容器中的所有容器均已准备就绪。 + */ + @K8sField("type") + private String type; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodMetrics.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodMetrics.java new file mode 100644 index 0000000..bd3e692 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizPodMetrics.java @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.fabric8.kubernetes.api.model.Duration; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.metrics.v1beta1.ContainerMetrics; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.enums.K8sKindEnum; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @description BizPodMetrics实体类 + * @date 2020-05-22 + */ +@Data +@Accessors(chain = true) +public class BizPodMetrics { + @K8sField("apiVersion") + private java.lang.String apiVersion = "metrics.k8s.io/v1beta1"; + + @K8sField("containers") + private List containers = new ArrayList<>(); + + @K8sField("kind") + private String kind = K8sKindEnum.PODMETRICS.getKind(); + + @K8sField("metadata") + private ObjectMeta metadata; + + @K8sField("timestamp") + private String timestamp; + + @K8sField("window") + private Duration window; + + @JsonIgnore + private Map additionalProperties = new HashMap<>(0); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizQuantity.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizQuantity.java new file mode 100644 index 0000000..5447d37 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizQuantity.java @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.utils.MathUtils; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description BizQuantity实体类 + * @date 2020-04-22 + */ +@Data +@Accessors(chain = true) +public class BizQuantity { + @K8sField("amount") + private String amount; + @K8sField("format") + private String format; + + public BizQuantity() { + + } + + public BizQuantity(String amount, String format) { + this.amount = amount; + this.format = format; + } + + public boolean isIllegal() { + return true; + } + + /** + * 单位相同时相减 + * @param bizQuantity 减数 + * @return + */ + public BizQuantity reduce(BizQuantity bizQuantity){ + if (bizQuantity == null || !bizQuantity.getFormat().equals(format)){ + return this; + } + return new BizQuantity(MathUtils.reduce(amount,bizQuantity.getAmount()),format); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizResourceQuota.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizResourceQuota.java new file mode 100644 index 0000000..369fc60 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizResourceQuota.java @@ -0,0 +1,77 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import org.springframework.util.CollectionUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @description ResourceQuota 业务类 + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class BizResourceQuota extends PtBaseResult { + @K8sField("apiVersion") + private String apiVersion; + @K8sField("kind") + private String kind; + + @K8sField("metadata:creationTimestamp") + private String creationTimestamp; + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("metadata:resourceVersion") + private String resourceVersion; + + @K8sField("status:hard") + private Map hard; + + @K8sField("status:used") + private Map used; + + @K8sField("spec:scopeSelector:matchExpressions") + private List matchExpressions; + + /** + * 获取余量 + * @return 余量列表 + */ + public Map getRemainder(){ + Map remainder = new HashMap<>(); + if (!CollectionUtils.isEmpty(hard)){ + for (Map.Entry entry : hard.entrySet()) { + if (used.get(entry.getKey()) != null){ + remainder.put(entry.getKey(),entry.getValue().reduce(used.get(entry.getKey()))); + } + } + } + return remainder; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizScopedResourceSelectorRequirement.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizScopedResourceSelectorRequirement.java new file mode 100644 index 0000000..280371c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizScopedResourceSelectorRequirement.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * @description BizScopedResourceSelectorRequirement实体类 + * @date 2020-04-23 + */ +@Data +@Accessors(chain = true) +public class BizScopedResourceSelectorRequirement { + @K8sField("operator") + private String operator; + @K8sField("scopeName") + private String scopeName; + @K8sField("values") + private List values; + + public BizScopedResourceSelectorRequirement(){ + + } + + public BizScopedResourceSelectorRequirement(String operator, String scopeName, List values) { + this.operator = operator; + this.scopeName = scopeName; + this.values = values; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizSecret.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizSecret.java new file mode 100644 index 0000000..054f51e --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizSecret.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description Kubernetes Secret + * @date 2020-09-09 + */ +@Data +@Accessors(chain = true) +public class BizSecret { + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizService.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizService.java new file mode 100644 index 0000000..c3a9c31 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizService.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description Kubernetes Service + * @date 2020-09-09 + */ +@Data +@Accessors(chain = true) +public class BizService { + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizTaint.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizTaint.java new file mode 100644 index 0000000..2b77f0f --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizTaint.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.annotation.K8sField; + +/** + * @description BizTaint 实体类 + * @date 2020-11-20 + */ +@Data +@Accessors(chain = true) +public class BizTaint { + @K8sField("effect") + private String effect; + @K8sField("key") + private String key; + @K8sField("timeAdded") + private String timeAdded; + @K8sField("value") + private String value; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolume.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolume.java new file mode 100644 index 0000000..aa9ca10 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolume.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizVolume实体类 + * @date 2020-04-15 + */ +@Data +@Accessors(chain = true) +public class BizVolume { + @K8sField("name") + private String name; + @K8sField("nfs:path") + private String path; + @K8sField("nfs:server") + private String server; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolumeMount.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolumeMount.java new file mode 100644 index 0000000..37f27d7 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/resource/BizVolumeMount.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.resource; + +import org.dubhe.k8s.annotation.K8sField; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @description BizVolumeMount实体类 + * @date 2020-04-15 + */ +@Data +@Accessors(chain = true) +public class BizVolumeMount { + @K8sField("mountPath") + private String mountPath; + @K8sField("name") + private String name; + @K8sField("readOnly") + private Boolean readOnly; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/GpuUsageVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/GpuUsageVO.java new file mode 100644 index 0000000..2f3a2d7 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/GpuUsageVO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * @description Gpu usage percent + * @date 2020-10-13 + */ +@Data +@AllArgsConstructor +public class GpuUsageVO { + /** + * 显卡id + */ + private String accId; + /** + * 使用率 百分比 + */ + Float usage; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/LogMonitoringVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/LogMonitoringVO.java new file mode 100644 index 0000000..68ff8a4 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/LogMonitoringVO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.dubhe.k8s.domain.PtBaseResult; +import java.util.List; + + +/** + * @description LogMonitoringVO对象 + * @date 2020-05-13 + */ +@Data +@AllArgsConstructor +public class LogMonitoringVO extends PtBaseResult { + private Long totalLogs; + private List logs; + + public LogMonitoringVO() { + + } + + public LogMonitoringVO(String code, String message){ + super(); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultVO.java new file mode 100644 index 0000000..0c8caa3 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultVO.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * @description 监控指标 VO + * @date 2021-02-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class MetricsDataResultVO { + /** + * 显卡编号 + */ + private String accId; + /** + * 监控指标值 + */ + List values; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultValueVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultValueVO.java new file mode 100644 index 0000000..0ca580b --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/MetricsDataResultValueVO.java @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * @description 监控指标数据 VO + * @date 2021-02-02 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class MetricsDataResultValueVO { + /** + * unix时间戳 + */ + private String time; + /** + * 值 + */ + private String value; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/ModelServingVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/ModelServingVO.java new file mode 100644 index 0000000..d59fb24 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/ModelServingVO.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.resource.BizDeployment; +import org.dubhe.k8s.domain.resource.BizIngress; +import org.dubhe.k8s.domain.resource.BizSecret; +import org.dubhe.k8s.domain.resource.BizService; + +/** + * @description 模型部署VO + * @date 2020-09-09 + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class ModelServingVO extends PtBaseResult { + private BizSecret bizSecret; + private BizService bizService; + private BizDeployment bizDeployment; + private BizIngress bizIngress; + + public ModelServingVO(BizSecret bizSecret, BizService bizService, BizDeployment bizDeployment, BizIngress bizIngress){ + this.bizSecret = bizSecret; + this.bizService = bizService; + this.bizDeployment = bizDeployment; + this.bizIngress = bizIngress; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodLogQueryVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodLogQueryVO.java new file mode 100644 index 0000000..82b02eb --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodLogQueryVO.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * @description Pod日志 查询响应VO + * @date 2020-08-14 + */ +@Data +@Accessors(chain = true) +@Api("Pod日志 查询响应VO") +@AllArgsConstructor +public class PodLogQueryVO { + + @ApiModelProperty("log内容") + private List content; + + @ApiModelProperty(value = "起始行") + private Integer startLine; + + @ApiModelProperty(value = "结束行") + private Integer endLine; + + @ApiModelProperty(value = "查询行数") + private Integer lines; + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodRangeMetricsVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodRangeMetricsVO.java new file mode 100644 index 0000000..359fae7 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodRangeMetricsVO.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * @description Pod历史监控指标 VO + * @date 2021-02-02 + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class PodRangeMetricsVO { + /** + * pod名称 + **/ + private String podName; + /** + * cpu 监控指标 value为使用百分比 + */ + List cpuMetrics; + /** + * gpu 监控指标 value为使用百分比 + */ + List gpuMetrics; + /** + * 内存 监控指标 value为占用内存 单位 Ki + */ + List memoryMetrics; + + public PodRangeMetricsVO(String podName){ + this.podName = podName; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodVO.java new file mode 100644 index 0000000..96fee79 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PodVO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.domain.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * @description Pod信息展示类 + * @date 2020-08-14 + */ +@Data +@AllArgsConstructor +@ApiModel("Pod信息展示类") +public class PodVO { + + @ApiModelProperty("K8s中Pod的真实名称") + private String podName; + + @ApiModelProperty("Pod展示名称") + private String displayName; + + @ApiModelProperty(value = "命名空间") + private String namespace; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtContainerMetricsVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtContainerMetricsVO.java new file mode 100644 index 0000000..750a39c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtContainerMetricsVO.java @@ -0,0 +1,98 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @description metrics result + * @date 2020-05-22 + */ +@Data +@Accessors(chain = true) +public class PtContainerMetricsVO implements Serializable { + private static final long serialVersionUID = 1L; + /** + * pod名称 + **/ + private String podName; + /** + * container名称 + **/ + private String containerName; + /** + * 时间 + **/ + private String timestamp; + /** + * cpu用量 + **/ + private String cpuUsageAmount; + /** + * cpu用量 单位 1核=1000m,1m=1000000n + **/ + private String cpuUsageFormat; + /** + * 内存用量 + **/ + private String memoryUsageAmount; + /** + * 内存用量单位 + **/ + private String memoryUsageFormat; + /****/ + private String nodeName; + + public PtContainerMetricsVO(String podName, String containerName, String timestamp, String cpuUsageAmount, String cpuUsageFormat, String memoryUsageAmount, String memoryUsageFormat) { + this.podName = podName; + this.containerName = containerName; + this.timestamp = timestamp; + this.cpuUsageAmount = cpuUsageAmount; + this.cpuUsageFormat = cpuUsageFormat; + this.memoryUsageAmount = memoryUsageAmount; + this.memoryUsageFormat = memoryUsageFormat; + } + + public PtContainerMetricsVO(String podName, String cpuUsageAmount, String cpuUsageFormat, String memoryUsageAmount, String memoryUsageFormat) { + this.podName = podName; + this.cpuUsageAmount = cpuUsageAmount; + this.cpuUsageFormat = cpuUsageFormat; + this.memoryUsageAmount = memoryUsageAmount; + this.memoryUsageFormat = memoryUsageFormat; + } + + /** + * 增加 cpuUsageAmount + * @param cpuUsageAmount + */ + public void addCpuUsageAmount(String cpuUsageAmount){ + this.cpuUsageAmount = String.valueOf(Long.valueOf(this.cpuUsageAmount)+Long.valueOf(cpuUsageAmount)); + } + + /** + * 增加 memoryUsageAmount + * @param memoryUsageAmount + */ + public void addMemoryUsageAmount(String memoryUsageAmount){ + this.memoryUsageAmount = String.valueOf(Long.valueOf(this.memoryUsageAmount)+Long.valueOf(memoryUsageAmount)); + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterDeployVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterDeployVO.java new file mode 100644 index 0000000..e71f522 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterDeployVO.java @@ -0,0 +1,129 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import cn.hutool.core.codec.Base64; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.api.model.extensions.HTTPIngressRuleValue; +import io.fabric8.kubernetes.api.model.extensions.Ingress; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.resource.BizContainer; +import org.dubhe.k8s.utils.MappingUtils; + +import java.util.List; +import java.util.Optional; + +/** + * @description Notebook deploy result + * @date 2020-04-17 + */ +@Data +@Accessors(chain = true) +public class PtJupyterDeployVO extends PtBaseResult { + + private String defaultJupyterPwd; + private String baseUrl; + private SecretInfo secretInfo; + private StatefulSetInfo statefulSetInfo; + private ServiceInfo serviceInfo; + private IngressInfo ingressInfo; + + public PtJupyterDeployVO() { + + } + + public PtJupyterDeployVO(Secret secret, StatefulSet statefulSet, Service service, Ingress ingress) { + Optional.ofNullable(secret).ifPresent(v -> { + String base64edPwd = v.getData().get(K8sParamConstants.SECRET_PWD_KEY); + String base64edBaseUrl = v.getData().get(K8sParamConstants.SECRET_URL_KEY); + this.defaultJupyterPwd = Base64.decodeStr(base64edPwd); + this.baseUrl = Base64.decodeStr(base64edBaseUrl) + SymbolConstant.SLASH; + this.secretInfo = MappingUtils.mappingTo(v, SecretInfo.class); + }); + Optional.ofNullable(statefulSet).ifPresent(v -> { + this.statefulSetInfo = MappingUtils.mappingTo(v, StatefulSetInfo.class); + }); + Optional.ofNullable(service).ifPresent(v -> { + this.serviceInfo = MappingUtils.mappingTo(v, ServiceInfo.class); + }); + Optional.ofNullable(ingress).ifPresent(v -> { + this.ingressInfo = MappingUtils.mappingTo(v, IngressInfo.class); + }); + } + + @Data + public static class SecretInfo { + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + } + + @Data + public static class StatefulSetInfo { + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("spec:template:metadata:uid") + private String podId; + @K8sField("spec:template:spec:containers") + private List containers; + } + + @Data + public static class ServiceInfo { + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + } + + @Data + public static class IngressInfo { + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("spec:rules") + private List rules; + } + + @Data + public static class BizIngressRule { + @K8sField("host") + private String host; + @K8sField("http") + private HTTPIngressRuleValue http; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterJobVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterJobVO.java new file mode 100644 index 0000000..b6543fe --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtJupyterJobVO.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.k8s.domain.PtBaseResult; +import org.dubhe.k8s.domain.resource.BizContainer; +import org.dubhe.k8s.utils.MappingUtils; +import io.fabric8.kubernetes.api.model.batch.Job; +import lombok.Data; + +import java.util.List; + +/** + * @description job result + * @date 2020-04-22 + */ +@Data +public class PtJupyterJobVO extends PtBaseResult { + + @K8sField("metadata:name") + private String name; + @K8sField("metadata:namespace") + private String namespace; + @K8sField("metadata:uid") + private String uid; + @K8sField("spec:template:metadata:uid") + private String podId; + @K8sField("spec:template:spec:containers") + private List containers; + + public static PtJupyterJobVO getInstance(Job job) { + return MappingUtils.mappingTo(job, PtJupyterJobVO.class); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtNodeMetricsVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtNodeMetricsVO.java new file mode 100644 index 0000000..c506c0e --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtNodeMetricsVO.java @@ -0,0 +1,66 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @description Node Metrics Result + * @date 2020-05-22 + */ +@Data +@Accessors(chain = true) +public class PtNodeMetricsVO implements Serializable { + private static final long serialVersionUID = 1L; + /** + * 节点名称 + **/ + private String nodeName; + /** + * 时间 + **/ + private String timestamp; + /** + * cpu用量 + **/ + private String cpuUsageAmount; + /** + * cpu用量 单位 1核=1000m,1m=1000000n + **/ + private String cpuUsageFormat; + /** + * 内存用量 + **/ + private String memoryUsageAmount; + /** + * 内存用量单位 + **/ + private String memoryUsageFormat; + + public PtNodeMetricsVO(String nodeName, String timestamp, String cpuUsageAmount, String cpuUsageFormat, String memoryUsageAmount, String memoryUsageFormat) { + this.nodeName = nodeName; + this.timestamp = timestamp; + this.cpuUsageAmount = cpuUsageAmount; + this.cpuUsageFormat = cpuUsageFormat; + this.memoryUsageAmount = memoryUsageAmount; + this.memoryUsageFormat = memoryUsageFormat; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtPodsVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtPodsVO.java new file mode 100644 index 0000000..5c746f3 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/PtPodsVO.java @@ -0,0 +1,151 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.MathUtils; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.k8s.utils.UnitConvertUtils; +import org.springframework.util.CollectionUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * @description 封装pod信息 + * @date 2020-06-03 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PtPodsVO implements Serializable { + private static final long serialVersionUID = 1L; + /** + * 命名空间 + */ + private String namespace; + /** + * pod名称 + **/ + private String podName; + /** + * cpu 申请量 + */ + private String cpuRequestAmount; + /** + * cpu用量 + **/ + private String cpuUsageAmount; + /** + * cpu 申请量 单位 1核=1000m,1m=1000000n + */ + private String cpuRequestFormat; + /** + * cpu用量 单位 1核=1000m,1m=1000000n + **/ + private String cpuUsageFormat; + /** + * cpu 使用百分比 + */ + private Float cpuUsagePercent; + /** + * 内存申请量 + */ + private String memoryRequestAmount; + /** + * 内存用量 + **/ + private String memoryUsageAmount; + /** + * 内存申请量单位 + **/ + private String memoryRequestFormat; + /** + * 内存用量单位 + **/ + private String memoryUsageFormat; + /** + * 内存使用百分比 + */ + private Float memoryUsagePercent; + /****/ + private String nodeName; + /** + * 设置status状态值 + **/ + private String status; + /** + * 设置gpu的使用情况 + **/ + private String gpuUsed; + /** + * gpu使用百分比 + */ + private List gpuUsagePersent; + + public PtPodsVO(String namespace,String podName,String cpuRequestAmount,String cpuUsageAmount,String cpuRequestFormat,String cpuUsageFormat,String memoryRequestAmount,String memoryUsageAmount,String memoryRequestFormat,String memoryUsageFormat,String nodeName,String status,String gpuUsed){ + this.namespace = namespace; + this.podName = podName; + this.cpuRequestAmount = cpuRequestAmount; + this.cpuUsageAmount = cpuUsageAmount; + this.cpuRequestFormat = cpuRequestFormat; + this.cpuUsageFormat = cpuUsageFormat; + this.memoryRequestAmount = memoryRequestAmount; + this.memoryUsageAmount = memoryUsageAmount; + this.memoryRequestFormat = memoryRequestFormat; + this.memoryUsageFormat = memoryUsageFormat; + this.nodeName = nodeName; + this.status = status; + this.gpuUsed = gpuUsed; + } + + public PtPodsVO(String namespace,String podName,String cpuUsageAmount,String cpuUsageFormat,String memoryUsageAmount,String memoryUsageFormat,String nodeName,String status,String gpuUsed){ + this.namespace = namespace; + this.podName = podName; + this.cpuUsageAmount = cpuUsageAmount; + this.cpuUsageFormat = cpuUsageFormat; + this.memoryUsageAmount = memoryUsageAmount; + this.memoryUsageFormat = memoryUsageFormat; + this.nodeName = nodeName; + this.status = status; + this.gpuUsed = gpuUsed; + } + + /** + * 计算资源使用百分比 + */ + public void calculationPercent(){ + if (StringUtils.isNotEmpty(cpuRequestAmount) && StringUtils.isNotEmpty(cpuUsageAmount)){ + cpuUsagePercent = MathUtils.floatDivision(UnitConvertUtils.cpuFormatToN(cpuUsageAmount,cpuUsageFormat).toString(),UnitConvertUtils.cpuFormatToN(cpuRequestAmount,cpuRequestFormat).toString(), MagicNumConstant.TWO) * MagicNumConstant.ONE_HUNDRED; + } + if (StringUtils.isNotEmpty(memoryRequestAmount) && StringUtils.isNotEmpty(memoryUsageAmount)){ + memoryUsagePercent = MathUtils.floatDivision(UnitConvertUtils.cpuFormatToN(memoryUsageAmount,memoryUsageFormat).toString(),UnitConvertUtils.cpuFormatToN(memoryRequestAmount,memoryRequestFormat).toString(), MagicNumConstant.TWO) * MagicNumConstant.ONE_HUNDRED; + } + } + + public void addGpuUsage(String accId,Float usage){ + if (CollectionUtils.isEmpty(gpuUsagePersent)){ + gpuUsagePersent = new ArrayList<>(); + } + gpuUsagePersent.add(new GpuUsageVO(accId,usage)); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/VolumeVO.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/VolumeVO.java new file mode 100644 index 0000000..2aad091 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/domain/vo/VolumeVO.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.domain.vo; + +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeMount; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.k8s.domain.PtBaseResult; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description 存储卷配置VO + * @date 2020-09-10 + */ +@Data +@Accessors(chain = true) +public class VolumeVO extends PtBaseResult { + private List volumeMounts; + private List volumes; + + /** + * 添加存储卷 + * @param volumeMount Kubernetes VolumeMount + */ + public void addVolumeMount(VolumeMount volumeMount){ + if (volumeMounts == null){ + volumeMounts = new ArrayList<>(); + } + volumeMounts.add(volumeMount); + } + + /** + * 添加存储卷 + * @param volume Kubernetes volume + */ + public void addVolume(Volume volume){ + if (volumes == null){ + volumes = new ArrayList<>(); + } + volumes.add(volume); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/AccessModeEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/AccessModeEnum.java new file mode 100644 index 0000000..c30002f --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/AccessModeEnum.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description AccessModes contains the desired access modes the volume should have + * @date 2020-06-19 + */ +public enum AccessModeEnum { + /** + * 单路读写 + */ + READ_WRITE_ONCE("ReadWriteOnce"), + /** + * 多路只读 + */ + READ_ONLY_MANY("ReadOnlyMany"), + /** + * 多路读写 + */ + READ_WRITE_MANY("ReadWriteMany"), + ; + + private String type; + + AccessModeEnum(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/BusinessLabelServiceNameEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/BusinessLabelServiceNameEnum.java new file mode 100644 index 0000000..66c24d0 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/BusinessLabelServiceNameEnum.java @@ -0,0 +1,90 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +import org.dubhe.biz.base.constant.ApplicationNameConst; +import org.dubhe.biz.base.enums.BizEnum; +import org.dubhe.biz.base.utils.StringUtils; + +import static org.dubhe.biz.base.constant.SymbolConstant.BLANK; + +/** + * @description 业务标签->服务名 映射枚举类 + * @date 2020-12-8 + */ +public enum BusinessLabelServiceNameEnum { + /** + * 模型开发 + */ + NOTEBOOK(BizEnum.NOTEBOOK.getBizCode(), ApplicationNameConst.SERVER_NOTEBOOK), + /** + * 训练管理 + */ + TRAIN(BizEnum.ALGORITHM.getBizCode(), ApplicationNameConst.SERVER_TRAIN), + /** + * 模型优化 + */ + MODEL_OPTIMIZE(BizEnum.MODEL_OPT.getBizCode(), ApplicationNameConst.SERVER_OPTIMIZE), + /** + * 云端Serving + */ + SERVING(BizEnum.SERVING.getBizCode(), ApplicationNameConst.SERVER_SERVING), + /** + * 批量服务 + */ + BATCH_SERVING(BizEnum.BATCH_SERVING.getBizCode(), ApplicationNameConst.SERVER_SERVING), + ; + /** + * 业务标签 + */ + private String businessLabel; + /** + * 服务名 + */ + private String serviceName; + + public String getBusinessLabel() { + return businessLabel; + } + + public String getServiceName() { + return serviceName; + } + + BusinessLabelServiceNameEnum(String businessLabel, String serviceName) { + this.businessLabel = businessLabel; + this.serviceName = serviceName; + } + public static String getServiceNameByBusinessLabel(String businessLabel){ + for (BusinessLabelServiceNameEnum businessLabelServiceNameEnum : BusinessLabelServiceNameEnum.values()) { + if (StringUtils.equals(businessLabel, businessLabelServiceNameEnum.getBusinessLabel() )){ + return businessLabelServiceNameEnum.getServiceName(); + } + } + return BLANK; + } + + public static String getBusinessLabelByServiceName(String serviceName){ + for (BusinessLabelServiceNameEnum businessLabelServiceNameEnum : BusinessLabelServiceNameEnum.values()) { + if (StringUtils.equals(serviceName, businessLabelServiceNameEnum.getServiceName() )){ + return businessLabelServiceNameEnum.getBusinessLabel(); + } + } + return BLANK; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/GraphicsCardTypeEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/GraphicsCardTypeEnum.java new file mode 100644 index 0000000..bf6c11e --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/GraphicsCardTypeEnum.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description graphics card type enum + * @date 2020-05-09 + */ +public enum GraphicsCardTypeEnum { + /** + * 显卡型号 英伟达泰坦v + */ + TITAN_V("Titan V", "英伟达泰坦v"), + /** + * 显卡型号 特斯拉v100 + */ + TESLA_V100("Tesla V100", "特斯拉v100"); + + GraphicsCardTypeEnum(String type, String caption) { + this.type = type; + this.caption = caption; + } + + private String type; + private String caption; + + public String getType() { + return type; + } + + public String getCaption() { + return caption; + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ImagePullPolicyEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ImagePullPolicyEnum.java new file mode 100644 index 0000000..e2b3536 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ImagePullPolicyEnum.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description 镜像拉取策略 + * @date 2020-07-01 + */ +public enum ImagePullPolicyEnum { + /** + * 默认值,本地有则使用本地镜像,不拉取 + */ + IFNOTPRESENT("IfNotPresent"), + /** + * 总是拉取 + */ + ALWAYS("Always"), + /** + * 只使用本地镜像,从不拉取 + */ + NEVER("Never"), + ; + + private String policy; + + ImagePullPolicyEnum(String policy) { + this.policy = policy; + } + + public String getPolicy(){ + return policy; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sKindEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sKindEnum.java new file mode 100644 index 0000000..8d057bd --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sKindEnum.java @@ -0,0 +1,76 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description k8s资源类型枚举 + * @date 2020-07-01 + */ +public enum K8sKindEnum { + /** + * Job + */ + JOB("Job"), + /** + * Pod + */ + POD("Pod"), + /** + * Deployment + */ + DEPLOYMENT("Deployment"), + /** + * StatefulSet + */ + STATEFULSET("StatefulSet"), + /** + * DistributeTrain + */ + DISTRIBUTETRAIN("DistributeTrain"), + /** + * StorageClass + */ + STORAGECLASS("StorageClass"), + /** + * PersistentVolumeClaim + */ + PERSISTENTVOLUMECLAIM("PersistentVolumeClaim"), + /** + * Namespace + */ + NAMESPACE("Namespace"), + /** + * PodMetrics + */ + PODMETRICS("PodMetrics"), + /** + * 原生混合资源类型 + */ + MixedNativeResource("MixedNativeResource"), + ; + + private String kind; + + K8sKindEnum(String kind) { + this.kind = kind; + } + + public String getKind(){ + return kind; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sResponseEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sResponseEnum.java new file mode 100644 index 0000000..d340d43 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sResponseEnum.java @@ -0,0 +1,82 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +import org.dubhe.k8s.domain.PtBaseResult; + +/** + * @description K8s response enum + * @date 2020-05-09 + */ +public enum K8sResponseEnum { + /** + * 成功 + */ + SUCCESS("200", ""), + + /** + * 没有对应的资源 + */ + NOT_FOUND("403", "请求的资源不存在"), + /** + * 创建,删除等操作的重复提交 + */ + REPEAT("404", "重复提交"), + /** + * 资源已存在 + */ + EXISTS("409", "资源已存在"), + /** + * 参数缺失 + */ + BAD_REQUEST("400", "参数缺失"), + /** + * 先决条件错误 + */ + PRECONDITION_FAILED("412", "先决条件错误"), + + /** + * k8s-client 内部错误 + */ + INTERNAL_SERVER_ERROR("500", "内部错误"), + /** + * 资源不足 + */ + LACK_OF_RESOURCES("516", "资源不足"), + ; + + K8sResponseEnum(String code, String message) { + this.code = code; + this.message = message; + } + + private String code; + private String message; + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public PtBaseResult toPtBaseResult() { + return new PtBaseResult(this.code, this.message); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTaskStatusEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTaskStatusEnum.java new file mode 100644 index 0000000..9a61727 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTaskStatusEnum.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.enums; + +/** + * @description k8s_task 状态枚举类 + * @date 2020-09-01 + */ +public enum K8sTaskStatusEnum { + /** + * 成功 + */ + NONE(0, "无需操作"), + UNEXECUTED(1, "待执行"), + EXECUTED(2, "已完成"), + ; + + private Integer status; + private String message; + K8sTaskStatusEnum(Integer status, String message) { + this.status = status; + this.message = message; + } + + + + public Integer getStatus() { + return status; + } + + public String getMessage() { + return message; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationEffectEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationEffectEnum.java new file mode 100644 index 0000000..0a161b5 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationEffectEnum.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description k8s Toleration effect 枚举 + * @date 2021-05-17 + */ +public enum K8sTolerationEffectEnum { + /** + * NoSchedule + */ + NOSCHEDULE("NoSchedule"), + /** + * PreferNoSchedule + */ + PREFERNOSCHEDULE("PreferNoSchedule"), + /** + * NoExecute + */ + NOEXECUTE("NoExecute"), + ; + + private String effect; + + K8sTolerationEffectEnum(String effect) { + this.effect = effect; + } + + public String getEffect() { + return effect; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationOperatorEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationOperatorEnum.java new file mode 100644 index 0000000..262c00d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/K8sTolerationOperatorEnum.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description k8s Toleration Operator 枚举 + * @date 2021-05-17 + */ +public enum K8sTolerationOperatorEnum { + /** + * Exists + */ + EXISTS("Exists"), + /** + * Equal + */ + EQUAL("Equal"), + ; + + private String operator; + + K8sTolerationOperatorEnum(String operator) { + this.operator = operator; + } + + public String getOperator() { + return operator; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LackOfResourcesEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LackOfResourcesEnum.java new file mode 100644 index 0000000..ddf3103 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LackOfResourcesEnum.java @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description 资源缺乏枚举类 + * @date 2020-06-22 + */ +public enum LackOfResourcesEnum { + /** + * 资源充足 + */ + ADEQUATE(0, "资源充足"), + /** + * cpu不足 + */ + LACK_OF_CPU(1, "cpu不足"), + /** + * 内存不足 + */ + LACK_OF_MEM(2, "内存不足"), + /** + * gpu不足 + */ + LACK_OF_GPU(3, "gpu不足"), + /** + * 没有可调度节点 + */ + LACK_OF_NODE(4, "没有可调度节点"), + ; + + LackOfResourcesEnum(int code, String message) { + this.code = code; + this.message = message; + } + + private int code; + private String message; + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LimitsOfResourcesEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LimitsOfResourcesEnum.java new file mode 100644 index 0000000..f91f29f --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/LimitsOfResourcesEnum.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description 资源超限枚举类 + * @date 2021-04-14 + */ +public enum LimitsOfResourcesEnum { + /** + * 资源充足 + */ + ADEQUATE(0, "资源充足"), + /** + * cpu不足 + */ + LIMITS_OF_CPU(1, "cpu用量超限"), + /** + * 内存不足 + */ + LIMITS_OF_MEM(2, "内存用量超限"), + /** + * gpu不足 + */ + LIMITS_OF_GPU(3, "gpu用量超限"), + ; + + LimitsOfResourcesEnum(int code, String message) { + this.code = code; + this.message = message; + } + + private int code; + private String message; + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/NodeConditionTypeEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/NodeConditionTypeEnum.java new file mode 100644 index 0000000..eb09996 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/NodeConditionTypeEnum.java @@ -0,0 +1,56 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description node健康状态枚举类 + * @date 2020-05-25 + */ +public enum NodeConditionTypeEnum { + /** + * 网络是否可用 + */ + NETWORKUNAVAILABLE("NetworkUnavailable"), + /** + * 内存压力 + */ + MEMORYPRESSURE("MemoryPressure"), + /** + * 磁盘压力 + */ + DISKPRESSURE("DiskPressure"), + /** + * PID压力 + */ + PIDPRESSURE("PIDPressure"), + /** + * node是否准备好 + */ + READY("Ready"), + ; + + private String type; + + NodeConditionTypeEnum(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PodPhaseEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PodPhaseEnum.java new file mode 100644 index 0000000..776c013 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PodPhaseEnum.java @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +import lombok.Getter; + +/** + * @description pod状态 + * @date 2020-05-14 + */ +@Getter +public enum PodPhaseEnum { + + /** + * Pending + */ + PENDING("Pending"), + /** + * Running + */ + RUNNING("Running"), + /** + * Succeeded + */ + SUCCEEDED("Succeeded"), + /** + * Deleted + */ + DELETED("Deleted"), + /** + * Failed + */ + FAILED("Failed"), + /** + * Unknown + */ + UNKNOWN("Unknown"), + ; + + private String phase; + + PodPhaseEnum(String phase) { + this.phase = phase; + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PvReclaimPolicyEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PvReclaimPolicyEnum.java new file mode 100644 index 0000000..9261cc8 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/PvReclaimPolicyEnum.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description PV回收策略枚举 + * @date 2020-07-01 + */ +public enum PvReclaimPolicyEnum { + /** + * 删除PV时,删除数据,只有 NFS 和 HostPath 支持 + */ + RECYCLE("Recycle"), + /** + * 不清理, 保留 Volume(需要手动清理) + */ + RETAIN("Retain"), + /** + * 删除存储资源,比如删除 AWS EBS 卷(只有 AWS EBS, GCE PD, Azure Disk 和 Cinder 支持) + */ + DELETE("Delete"), + ; + + private String policy; + + PvReclaimPolicyEnum(String policy) { + this.policy = policy; + } + + public String getPolicy(){ + return policy; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/RestartPolicyEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/RestartPolicyEnum.java new file mode 100644 index 0000000..1d27f7c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/RestartPolicyEnum.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description k8s pod restart policy enum + * @date 2020-05-29 + */ +public enum RestartPolicyEnum { + /** + * 当容器退出时,总是重启容器,默认策略 + */ + ALWAYS("Always"), + /** + * 当容器异常退出(退出状态码非0)时,重启容器 + */ + ONFAILURE("OnFailure"), + /** + * 当容器退出时,从不重启容器 + */ + NEVER("Never"), + ; + + private String restartPolicy; + + RestartPolicyEnum(String restartPolicy) { + this.restartPolicy = restartPolicy; + } + + public String getRestartPolicy() { + return restartPolicy; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ShellCommandEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ShellCommandEnum.java new file mode 100644 index 0000000..5ef19f8 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ShellCommandEnum.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description command的shell类型枚举 + * @date 2020-07-01 + */ +public enum ShellCommandEnum { + /** + * /bin/bash + */ + BIN_BANSH("/bin/bash"), + /** + * + */ + BASH("bash"), + ; + + private String shell; + + ShellCommandEnum(String shell) { + this.shell = shell; + } + + public String getShell(){ + return shell; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/TopLogInfoEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/TopLogInfoEnum.java new file mode 100644 index 0000000..8f7eb6c --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/TopLogInfoEnum.java @@ -0,0 +1,73 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +import lombok.Getter; +import org.dubhe.biz.base.utils.StringUtils; + +/** + * @description TopLogInfoEnum枚举类 + * @date 2020-06-30 + */ +@Getter +public enum TopLogInfoEnum { + + /** + * Succeeded + */ + SUCCESSED("Succeeded","任务日志为空"), + /** + * Pending + */ + PENDING("Pending","No log or log file has expired"), + /** + * Running + */ + RUNNING("Running",""), + /** + * Failed + */ + FAILED("Failed","No log or log file has expired"), + /** + * Unknown + */ + UNKNOWN("Unknown","No log or log file has expired"), + /** + * NotExist + */ + NOT_EXIST("NotExist","No log or log file has expired") + + ; + private String status; + private String info; + + TopLogInfoEnum(String status,String info) { + this.status = status; + this.info = info; + } + + public static TopLogInfoEnum getTopLogInfoEnum(String status){ + for (TopLogInfoEnum topLogInfoEnum : TopLogInfoEnum.values()) { + if (StringUtils.equals(status,topLogInfoEnum.getStatus())){ + return topLogInfoEnum; + } + } + return NOT_EXIST; + + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ValidationTypeEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ValidationTypeEnum.java new file mode 100644 index 0000000..477929d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/ValidationTypeEnum.java @@ -0,0 +1,29 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +/** + * @description 字段校验类型 + * @date 2020-06-04 + */ +public enum ValidationTypeEnum { + /** + * k8s资源对象名称校验 + */ + K8S_RESOURCE_NAME +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/WatcherActionEnum.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/WatcherActionEnum.java new file mode 100644 index 0000000..2b634af --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/enums/WatcherActionEnum.java @@ -0,0 +1,63 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.enums; + +import org.dubhe.biz.base.utils.StringUtils; + +/** + * @description k8s event watcher action enum + * @date 2020-06-02 + */ +public enum WatcherActionEnum { + /** + * 添加 + */ + ADDED("ADDED"), + /** + * 修改 + */ + MODIFIED("MODIFIED"), + /** + * 删除 + */ + DELETED("DELETED"), + /** + * 错误 + */ + ERROR("ERROR"), + ; + + private String action; + + WatcherActionEnum(String action) { + this.action = action; + } + + public String getAction() { + return action; + } + + public static WatcherActionEnum get(String action) { + for (WatcherActionEnum watcherActionEnum : WatcherActionEnum.values()) { + if (StringUtils.equals(action, watcherActionEnum.getAction())) { + return watcherActionEnum; + } + } + return null; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/interceptor/K8sCallBackPodInterceptor.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/interceptor/K8sCallBackPodInterceptor.java new file mode 100644 index 0000000..324fb8a --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/interceptor/K8sCallBackPodInterceptor.java @@ -0,0 +1,57 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.interceptor; + +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.utils.K8sCallBackTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @description k8s pod 异步回调拦截器 + * @date 2020-05-28 + */ +@Component +public class K8sCallBackPodInterceptor extends HandlerInterceptorAdapter { + + @Autowired + private K8sCallBackTool k8sCallBackTool; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String uri = request.getRequestURI(); + LogUtil.debug(LogEnum.BIZ_K8S,"接收到k8s异步请求,URI:{}",uri); + String k8sCallbackToken = request.getHeader(K8sCallBackTool.K8S_CALLBACK_TOKEN); + if (StringUtils.isBlank(k8sCallbackToken)){ + LogUtil.warn(LogEnum.BIZ_K8S,"k8s异步回调没有配置【{}】,URI:{}",K8sCallBackTool.K8S_CALLBACK_TOKEN,uri); + return false; + } + boolean pass = k8sCallBackTool.validateToken(k8sCallbackToken); + if (!pass){ + LogUtil.warn(LogEnum.BIZ_K8S,"k8s异步回调token:【{}】 验证不通过,URI:{}",k8sCallbackToken,uri); + } + return pass; + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/properties/ClusterProperties.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/properties/ClusterProperties.java new file mode 100644 index 0000000..a2b3385 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/properties/ClusterProperties.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; + +/** + * @description k8s connection properties + * @date 2020-04-09 + */ +@Data +@ConfigurationProperties("k8s") +public class ClusterProperties { + + private String url; + + private String nfs; + + private String host; + + private String port; + + private String kubeconfig; + + /** + * 需使用Resource + */ + private Resource clientCrt; + + private Resource clientKey; + + private Resource caCrt; +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/DeploymentCallbackAsyncService.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/DeploymentCallbackAsyncService.java new file mode 100644 index 0000000..298a7bc --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/DeploymentCallbackAsyncService.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.service; + +import org.dubhe.k8s.domain.dto.BaseK8sDeploymentCallbackCreateDTO; +import org.springframework.scheduling.annotation.Async; + +/** + * @description deployment 异步回调 + * @date 2020-11-27 + */ +public interface DeploymentCallbackAsyncService { + + /** + * deployment 异步回调 + * @param k8sDeploymentCallbackCreateDTO + */ + @Async + void deploymentCallBack (R k8sDeploymentCallbackCreateDTO); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sResourceService.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sResourceService.java new file mode 100644 index 0000000..7405207 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sResourceService.java @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.service; + +import org.dubhe.k8s.domain.entity.K8sResource; +import org.dubhe.k8s.domain.resource.BizPod; + +import java.util.List; + +/** + * @description k8s资源服务接口 + * @date 2020-07-13 + */ +public interface K8sResourceService { + /** + * 根据pod插入 + * + * @param pod Pod对象 + * @return int 插入数量 + */ + int create(BizPod pod); + + /** + * 新增 + * + * @param k8sResource 对象 + * @return int 插入数量 + */ + int create(K8sResource k8sResource); + + /** + * 根据资源名查询 + * + * @param kind 资源类型 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return List K8sResource资源集合 + */ + List selectByResourceName(String kind, String namespace, String resourceName); + + /** + * 根据对象名查询 + * + * @param kind 资源类型 + * @param namespace 命名空间 + * @param name 资源名称 + * @return List K8sResource资源集合 + */ + List selectByName(String kind, String namespace, String name); + + /** + * 根据resourceName删除 + * @param kind 资源类型 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return int 删除数量 + */ + int deleteByResourceName(String kind,String namespace,String resourceName); + + /** + * 根据名称删除 + * @param kind + * @param namespace + * @param name 比如kind 是 pod那name就对应 podName + * @return int 删除数量 + */ + int deleteByName(String kind,String namespace,String name); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sTaskService.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sTaskService.java new file mode 100644 index 0000000..5c56c7e --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/K8sTaskService.java @@ -0,0 +1,85 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.service; + +import org.dubhe.k8s.domain.bo.K8sTaskBO; +import org.dubhe.k8s.domain.entity.K8sTask; + +import java.util.List; + +/** + * @description k8s任务服务 + * @date 2020-08-31 + */ +public interface K8sTaskService { + /** + * 创建或者更新任务 + * + * @param k8sTask 对象 + * @return int 插入数量 + */ + int createOrUpdateTask(K8sTask k8sTask); + + /** + * 修改任务 + * @param k8sTask k8s任务 + * @return int 更新数量 + */ + int update(K8sTask k8sTask); + + /** + * 根据namesapce 和 resourceName 查询 + * @param k8sTask + * @return + */ + List selectByNamespaceAndResourceName(K8sTask k8sTask); + + /** + * 根据条件查询未执行的任务 + * @param k8sTaskBO k8s任务参数 + * @return List k8s任务类集合 + */ + List selectUnexecutedTask(K8sTaskBO k8sTaskBO); + + /** + * 查询未执行的任务 + * @return List k8s任务类集合 + */ + List selectUnexecutedTask(); + + /** + * 添加redis延时队列 + * @param k8sTask + * @return + */ + boolean addRedisDelayTask(K8sTask k8sTask); + + /** + * 加载任务到延时队列 + */ + void loadTaskToRedis(); + + /** + * 根据namespace 和 resourceName 删除 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return boolean + */ + boolean deleteByNamespaceAndResourceName(String namespace,String resourceName); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodCallbackAsyncService.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodCallbackAsyncService.java new file mode 100644 index 0000000..67171ea --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodCallbackAsyncService.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.service; + +import org.dubhe.k8s.domain.dto.BaseK8sPodCallbackCreateDTO; +import org.springframework.scheduling.annotation.Async; + +/** + * @description Pod 异步回调处理接口 + * @date 2020-05-28 + */ +public interface PodCallbackAsyncService { + + /** + * pod 异步回调 + * @param k8sPodCallbackCreateDTO + */ + @Async + void podCallBack (R k8sPodCallbackCreateDTO); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodService.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodService.java new file mode 100644 index 0000000..2222b72 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/PodService.java @@ -0,0 +1,69 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.service; + +import org.dubhe.k8s.domain.dto.PodLogDownloadQueryDTO; +import org.dubhe.k8s.domain.dto.PodLogQueryDTO; +import org.dubhe.k8s.domain.dto.PodQueryDTO; +import org.dubhe.k8s.domain.vo.PodLogQueryVO; +import org.dubhe.k8s.domain.vo.PodVO; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; + +/** + * @description Pod业务接口 + * @date 2020-08-14 + */ +public interface PodService { + + /** + * 查询Pod信息 + * @param podQueryDTO pod查询条件对象 + * @return List pod对象集合 + */ + List getPods(PodQueryDTO podQueryDTO); + + /** + * 按Pod名称分页查询Pod日志 + * @param podLogQueryDTO pod日志查询条件对象 + * @return PodLogQueryVO Pod日志 + */ + PodLogQueryVO getPodLog(PodLogQueryDTO podLogQueryDTO); + + /** + * 获取单pod日志字符串 + * @param podLogQueryDTO pod日志查询条件对象 + * @return String Pod日志 + */ + String getPodLogStr(PodLogQueryDTO podLogQueryDTO); + + /** + * 按Pod名称下载日志 + * @param podLogDownloadQueryDTO pod日志下载查询对象 + * @param response 响应 + */ + void downLoadPodLog(PodLogDownloadQueryDTO podLogDownloadQueryDTO, HttpServletResponse response); + + /** + * 统计Pod日志数量 + * @param podLogDownloadQueryDTO 日志下载查询对象 + * @return Map String:Pod name,Long: Pod count + */ + Map getLogCount(PodLogDownloadQueryDTO podLogDownloadQueryDTO); +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sResourceServiceImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sResourceServiceImpl.java new file mode 100644 index 0000000..b9511c0 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sResourceServiceImpl.java @@ -0,0 +1,163 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.dao.K8sResourceMapper; +import org.dubhe.k8s.domain.entity.K8sResource; +import org.dubhe.k8s.domain.resource.BizPod; +import org.dubhe.k8s.enums.K8sKindEnum; +import org.dubhe.k8s.service.K8sResourceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description k8s资源服务实现类 + * @date 2020-07-13 + */ +@Service +public class K8sResourceServiceImpl implements K8sResourceService { + @Autowired + private K8sResourceMapper k8sResourceMapper; + + /** + * 根据pod插入 + * + * @param pod Pod对象 + * @return int 插入数量 + */ + @Override + public int create(BizPod pod) { + return create(new K8sResource(K8sKindEnum.POD.getKind(),pod.getNamespace(),pod.getName(),pod.getLabel(K8sLabelConstants.BASE_TAG_SOURCE), SpringContextHolder.getActiveProfile(),pod.getBusinessLabel())); + } + + /** + * 插入 + * + * @param k8sResource + * @return int 插入数量 + */ + @Override + public int create(K8sResource k8sResource) { + if (null == k8sResource){ + return 0; + } + QueryWrapper queryK8sResourceJonWrapper = new QueryWrapper<>(k8sResource); + List list = k8sResourceMapper.selectList(queryK8sResourceJonWrapper); + if (CollectionUtil.isEmpty(list)){ + LogUtil.info(LogEnum.BIZ_K8S,"insert k8sResource:{}",k8sResource); + return k8sResourceMapper.insert(k8sResource); + }else { + LogUtil.warn(LogEnum.BIZ_K8S,"k8sResource already exist:{}",k8sResource); + return 0; + } + } + + /** + * 根据资源名查询 + * + * @param kind 资源类型 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return List K8sResource集合 + */ + @Override + public List selectByResourceName(String kind, String namespace, String resourceName) { + if (StringUtils.isEmpty(kind) || StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceName)) { + return new ArrayList<>(0); + } + QueryWrapper queryK8sResourceJonWrapper = new QueryWrapper<>(); + queryK8sResourceJonWrapper.eq("kind", kind) + .eq("namespace", namespace) + .eq("resource_name", resourceName) + .eq("env", SpringContextHolder.getActiveProfile()) + .eq("deleted", 0) + .orderByDesc("create_time"); + return k8sResourceMapper.selectList(queryK8sResourceJonWrapper); + } + + /** + * 根据对象名查询 + * + * @param kind 资源名称 + * @param namespace 命名空间 + * @param name 资源名字 + * @return List K8sResource集合 + */ + @Override + public List selectByName(String kind, String namespace, String name) { + if (StringUtils.isEmpty(kind) || StringUtils.isEmpty(namespace) || StringUtils.isEmpty(name)) { + return new ArrayList<>(0); + } + QueryWrapper queryK8sResourceJonWrapper = new QueryWrapper<>(); + queryK8sResourceJonWrapper.eq("kind", kind) + .eq("namespace", namespace) + .eq("name", name) + .eq("env", SpringContextHolder.getActiveProfile()) + .eq("deleted", 0) + .orderByDesc("create_time"); + return k8sResourceMapper.selectList(queryK8sResourceJonWrapper); + } + + /** + * 根据resourceName删除 + * @param kind 资源类型 + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return int 删除数量 + */ + @Override + public int deleteByResourceName(String kind, String namespace, String resourceName) { + if (StringUtils.isEmpty(kind) || StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceName)) { + return 0; + } + UpdateWrapper updateK8sResourceJonWrapper = new UpdateWrapper<>(); + updateK8sResourceJonWrapper.eq("kind", kind) + .eq("namespace", namespace) + .eq("resource_name", resourceName) + .eq("env", SpringContextHolder.getActiveProfile()) + .eq("deleted", 0).set("deleted", 1); + + return k8sResourceMapper.update(null,updateK8sResourceJonWrapper); + } + + @Override + public int deleteByName(String kind, String namespace, String name) { + if (StringUtils.isEmpty(kind) || StringUtils.isEmpty(namespace) || StringUtils.isEmpty(name)) { + return 0; + } + UpdateWrapper updateK8sResourceJonWrapper = new UpdateWrapper<>(); + updateK8sResourceJonWrapper.eq("kind", kind) + .eq("namespace", namespace) + .eq("name", name) + .eq("env", SpringContextHolder.getActiveProfile()) + .eq("deleted", 0).set("deleted", 1); + + return k8sResourceMapper.update(null,updateK8sResourceJonWrapper); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sTaskServiceImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sTaskServiceImpl.java new file mode 100644 index 0000000..786c9d6 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/K8sTaskServiceImpl.java @@ -0,0 +1,179 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.redis.utils.RedisUtils; +import org.dubhe.k8s.constant.RedisConstants; +import org.dubhe.k8s.dao.K8sTaskMapper; +import org.dubhe.k8s.domain.bo.K8sTaskBO; +import org.dubhe.k8s.domain.entity.K8sTask; +import org.dubhe.k8s.enums.K8sTaskStatusEnum; +import org.dubhe.k8s.service.K8sTaskService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description k8s任务服务实现类 + * @date 2020-08-31 + */ +@Service +public class K8sTaskServiceImpl implements K8sTaskService { + + @Autowired + K8sTaskMapper k8sTaskMapper; + @Autowired + private RedisUtils redisUtils; + /** + * 创建或者更新任务 + * + * @param k8sTask 对象 + * @return int 插入数量 + */ + @Override + public int createOrUpdateTask(K8sTask k8sTask) { + if (k8sTask == null){ + return 0; + } + List oldK8sTaskList = selectByNamespaceAndResourceName(k8sTask); + if (CollectionUtils.isEmpty(oldK8sTaskList)){ + addRedisDelayTask(k8sTask); + return k8sTaskMapper.insertOrUpdate(k8sTask); + }else { + k8sTask.setId(oldK8sTaskList.get(0).getId()); + k8sTask.setDeleted(false); + addRedisDelayTask(k8sTask); + return update(k8sTask); + } + } + /** + * 修改任务 + * @param k8sTask k8s任务 + * @return int 更新数量 + */ + @Override + public int update(K8sTask k8sTask) { + if (k8sTask == null){ + return 0; + } + addRedisDelayTask(k8sTask); + if (k8sTask.getId() != null){ + return k8sTaskMapper.updateById(k8sTask); + } + List oldK8sTaskList = selectByNamespaceAndResourceName(k8sTask); + if (!CollectionUtils.isEmpty(oldK8sTaskList)){ + k8sTask.setId(oldK8sTaskList.get(0).getId()); + k8sTask.setDeleted(false); + return k8sTaskMapper.updateById(k8sTask); + } + return 0; + } + + /** + * 根据namesapce 和 resourceName 查询 + * @param k8sTask + * @return + */ + @Override + public List selectByNamespaceAndResourceName(K8sTask k8sTask){ + if (k8sTask == null || StringUtils.isEmpty(k8sTask.getNamespace()) || StringUtils.isEmpty(k8sTask.getResourceName())){ + return null; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(K8sTask::getNamespace,k8sTask.getNamespace()); + queryWrapper.eq(K8sTask::getResourceName,k8sTask.getResourceName()); + + return k8sTaskMapper.selectList(queryWrapper); + } + + /** + * 查询 + * @param k8sTaskBO k8s任务参数 + * @return List k8s任务类集合 + */ + @Override + public List selectUnexecutedTask(K8sTaskBO k8sTaskBO) { + if (k8sTaskBO == null){ + return new ArrayList<>(); + } + return k8sTaskMapper.selectUnexecutedTask(k8sTaskBO); + } + + @Override + public List selectUnexecutedTask() { + K8sTaskBO k8sTaskBO = new K8sTaskBO(); + Long curUnixTime = System.currentTimeMillis()/ MagicNumConstant.ONE_THOUSAND; + k8sTaskBO.setMaxApplyUnixTime(curUnixTime); + k8sTaskBO.setMaxStopUnixTime(curUnixTime); + k8sTaskBO.setApplyStatus(K8sTaskStatusEnum.UNEXECUTED.getStatus()); + k8sTaskBO.setStopStatus(K8sTaskStatusEnum.UNEXECUTED.getStatus()); + LogUtil.info(LogEnum.BIZ_K8S,"selectUnexecutedTask {}", JSON.toJSONString(k8sTaskBO)); + return selectUnexecutedTask(k8sTaskBO); + } + + /** + * 添加redis延时队列 + * @param k8sTask + * @return + */ + @Override + public boolean addRedisDelayTask(K8sTask k8sTask) { + if (k8sTask == null || StringUtils.isEmpty(k8sTask.getNamespace()) || StringUtils.isEmpty(k8sTask.getResourceName())){ + return false; + } + boolean success = true; + if (k8sTask.getApplyUnixTime() != null && k8sTask.getApplyUnixTime() > 0){ + success &= redisUtils.zAdd(RedisConstants.DELAY_APPLY_ZSET_KEY,k8sTask.getApplyUnixTime(), String.format(RedisConstants.DELAY_ZSET_VALUE,k8sTask.getNamespace(),k8sTask.getResourceName())); + } + if (k8sTask.getStopUnixTime() != null && k8sTask.getStopUnixTime() > 0){ + success &= redisUtils.zAdd(RedisConstants.DELAY_STOP_ZSET_KEY,k8sTask.getStopUnixTime(),String.format(RedisConstants.DELAY_ZSET_VALUE,k8sTask.getNamespace(),k8sTask.getResourceName())); + } + return success; + } + + /** + * 加载任务到延时队列 + */ + @Override + public void loadTaskToRedis(){ + selectUnexecutedTask().forEach(x->addRedisDelayTask(x)); + } + + /** + * 根据namespace 和 resourceName 删除 + * + * @param namespace 命名空间 + * @param resourceName 资源名称 + * @return boolean + */ + @Override + public boolean deleteByNamespaceAndResourceName(String namespace, String resourceName) { + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceName)){ + return false; + } + return k8sTaskMapper.deleteByNamespaceAndResourceName(namespace,resourceName,true) > 0 ? true: false; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/PodServiceImpl.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/PodServiceImpl.java new file mode 100644 index 0000000..602d0f6 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/service/impl/PodServiceImpl.java @@ -0,0 +1,277 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.permission.base.BaseService; +import org.dubhe.k8s.api.LogMonitoringApi; +import org.dubhe.k8s.cache.ResourceCache; +import org.dubhe.k8s.domain.bo.LogMonitoringBO; +import org.dubhe.k8s.domain.dto.PodLogDownloadQueryDTO; +import org.dubhe.k8s.domain.dto.PodLogQueryDTO; +import org.dubhe.k8s.domain.dto.PodQueryDTO; +import org.dubhe.k8s.domain.vo.LogMonitoringVO; +import org.dubhe.k8s.domain.vo.PodLogQueryVO; +import org.dubhe.k8s.domain.vo.PodVO; +import org.dubhe.k8s.service.PodService; +import org.dubhe.k8s.utils.K8sNameTool; +import org.dubhe.k8s.utils.PodUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description Pod业务接口 + * @date 2020-08-14 + */ +@Service +public class PodServiceImpl implements PodService { + + @Autowired + private ResourceCache resourceCache; + + @Autowired + private K8sNameTool k8sNameTool; + + @Autowired + private LogMonitoringApi logMonitoringApi; + + @Autowired + private UserContextService userContextService; + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + /** + * 查询Pod信息 + * @param podQueryDTO + * @return List Pod信息 + */ + @Override + public List getPods(PodQueryDTO podQueryDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + String namespace = podQueryDTO.getNamespace(); + if (!BaseService.isAdmin(user)) { + checkNamespace(namespace, user.getId()); + } + List podVOList = new ArrayList<>(); + Set podSet = resourceCache.getPodNameByResourceName(namespace, podQueryDTO.getResourceName()); + if (CollectionUtils.isEmpty(podSet)) { + return podVOList; + } + List podList = new ArrayList<>(podSet); + Collections.sort(podList); + int masterIndex = 1; + int slaveIndex = 1; + int podIndex = 1; + + for (String pod : podList) { + if (PodUtil.isMaster(pod)) { + podVOList.add(new PodVO(pod, "Master" + (masterIndex++), namespace)); + } else if (PodUtil.isSlave(pod)) { + podVOList.add(new PodVO(pod, "Slave" + (slaveIndex++), namespace)); + } else { + podVOList.add(new PodVO(pod, "Pod" + (podIndex++), namespace)); + } + } + Collections.sort(podVOList, Comparator.comparing(PodVO::getDisplayName)); + return podVOList; + } + + /** + * 按Pod名称分页查询Pod日志 + * @param podLogQueryDTO pod日志查询条件实体类 + * @return PodLogQueryVO Pod日志 + */ + @Override + public PodLogQueryVO getPodLog(PodLogQueryDTO podLogQueryDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + String namespace = podLogQueryDTO.getNamespace(); + if (!BaseService.isAdmin(user)) { + checkNamespace(namespace, user.getId()); + } + LogMonitoringVO result = getLogMonitoringVO(podLogQueryDTO); + return new PodLogQueryVO(result.getLogs(), + podLogQueryDTO.getQueryStart(), + podLogQueryDTO.getQueryStart() + result.getTotalLogs().intValue() - 1, + result.getTotalLogs().intValue()); + } + + /** + * 获取单pod日志字符串 + * @param podLogQueryDTO pod日志查询条件实体类 + * @return String Pod日志 + */ + @Override + public String getPodLogStr(PodLogQueryDTO podLogQueryDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + String namespace = podLogQueryDTO.getNamespace(); + if (!BaseService.isAdmin(user)) { + checkNamespace(namespace, user.getId()); + } + return getPodLogStr(namespace, podLogQueryDTO); + } + + /** + * 获取单pod日志字符串 + * @param namespace 命名空间 + * @param podLogQueryDTO pod日志查询实体类 + * @return String Pod日志 + */ + private String getPodLogStr(String namespace, PodLogQueryDTO podLogQueryDTO) { + StringBuilder logBuilder = new StringBuilder(); + LogMonitoringVO result = getLogMonitoringVO(namespace, podLogQueryDTO); + // 当前查询总条数 + int curQueryLogSize = 0; + // 计划查询总条数 + Integer toQueryLogSize = podLogQueryDTO.getLines(); + while (CollectionUtil.isNotEmpty(result.getLogs())) { + result.getLogs().forEach(log -> logBuilder.append(log).append(StrUtil.CRLF)); + curQueryLogSize += result.getLogs().size(); + // 设置下次查询起始坐标 = 上次查询坐标+上次查询跨度 + podLogQueryDTO.setStartLine(podLogQueryDTO.getQueryStart() + podLogQueryDTO.getQueryLines()); + if (toQueryLogSize != null) { + // 当前有计划查询总条数 + if (curQueryLogSize >= toQueryLogSize) { + // 满足计划查询总条数,查询完毕 + break; + } else if (curQueryLogSize + podLogQueryDTO.getQueryLines() > toQueryLogSize) { + // 若本批次查询会超出计划查询总条数,则取 计划查询总条数 - 当前查询总条数 作为本批次查询条数 + podLogQueryDTO.setLines(toQueryLogSize - curQueryLogSize); + } + } + result = getLogMonitoringVO(namespace, podLogQueryDTO); + } + return logBuilder.toString(); + } + + /** + * 查询Pod日志 + * @param podLogQueryDTO pod日志查询条件实体类 + * @return LogMonitoringVO Pod日志 + */ + private LogMonitoringVO getLogMonitoringVO(PodLogQueryDTO podLogQueryDTO) { + return getLogMonitoringVO(podLogQueryDTO.getNamespace(), podLogQueryDTO); + } + + /** + * 查询Pod日志 + * @param namespace 命名空间 + * @param podLogQueryDTO pod日志查询实体类 + * @return LogMonitoringVO Pod日志 + */ + private LogMonitoringVO getLogMonitoringVO(String namespace, PodLogQueryDTO podLogQueryDTO) { + LogMonitoringBO logMonitoringBo = new LogMonitoringBO(namespace, podLogQueryDTO); + return logMonitoringApi.searchLogByPodName( + podLogQueryDTO.getQueryStart(), + podLogQueryDTO.getQueryLines(), + logMonitoringBo); + } + + /** + * 按Pod名称下载日志 + * @param podLogDownloadQueryDTO pod日志下载条件实体类 + * @param response 响应 + */ + @Override + public void downLoadPodLog(PodLogDownloadQueryDTO podLogDownloadQueryDTO, HttpServletResponse response) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + String namespace = podLogDownloadQueryDTO.getNamespace(); + if (!BaseService.isAdmin(user)) { + checkNamespace(namespace, user.getId()); + } + String random = DateUtil.format(new Date(), DatePattern.PURE_DATETIME_FORMAT) + RandomUtil.randomString(4); + String baseTempPath = System.getProperty("java.io.tmpdir"); + // 临时目录 + String tempPath = baseTempPath + File.separator + random; + fileStoreApi.createDir(tempPath); + for (PodVO podVO : podLogDownloadQueryDTO.getPodVOList()) { + // 按节点名称获取日志 + String podLogStr = getPodLogStr(namespace, new PodLogQueryDTO(podVO.getPodName())); + // 生成Pod日志文件 + String podLogFilePath = tempPath + File.separator + podVO.getDisplayName() + ".log"; + fileStoreApi.createOrAppendFile(podLogFilePath, podLogStr, true); + } + // 压缩文件 (与临时目录同目录级) + String zipFile = tempPath + ".zip"; + fileStoreApi.zipDirOrFile(tempPath, zipFile); + // 下载文件 + fileStoreApi.download(zipFile, response); + // 删除临时文件 + fileStoreApi.deleteDirOrFile(zipFile); + fileStoreApi.deleteDirOrFile(tempPath); + } + + /** + * 统计Pod日志数量 + * @param podLogDownloadQueryDTO pod日志下载条件实体类 + * @return Map String:Pod name,Long: Pod count + */ + @Override + public Map getLogCount(PodLogDownloadQueryDTO podLogDownloadQueryDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + String namespace = podLogDownloadQueryDTO.getNamespace(); + if (!BaseService.isAdmin(user)) { + checkNamespace(namespace, user.getId()); + } + Map logCountMap = new HashMap<>(podLogDownloadQueryDTO.getPodVOList().size() * 2); + for (int i = 0; i < podLogDownloadQueryDTO.getPodVOList().size(); i++) { + String podName = podLogDownloadQueryDTO.getPodVOList().get(i).getPodName(); + logCountMap.put(podName, logMonitoringApi.searchLogCountByPodName(new LogMonitoringBO(namespace, podName))); + } + return logCountMap; + } + + /** + * namespace 命名空间名称校验 + * + * + */ + private void checkNamespace(String namespace, Long curUserId) { + if (curUserId == null) { + throw new RuntimeException("Please Login!"); + } + if (!namespace.equals(k8sNameTool.generateNamespace(curUserId))) { + throw new RuntimeException("权限不足"); + } + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/BizConvertUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/BizConvertUtils.java new file mode 100644 index 0000000..87636bf --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/BizConvertUtils.java @@ -0,0 +1,268 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import cn.hutool.core.collection.CollectionUtil; +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.batch.Job; +import io.fabric8.kubernetes.api.model.extensions.Ingress; +import org.dubhe.k8s.domain.cr.DistributeTrain; +import org.dubhe.k8s.domain.resource.*; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @description Biz转换工具类,统一在此进行fabric8 POJO到Biz的转换,以保证各处转换结果一致 + * @date 2020-06-02 + * */ +public class BizConvertUtils { + private static final String COMPLETED = "Completed"; + + /** + * 将Pod List转为 PodBiz List + * + * @param podList Pod的集合 + * @return List BizPod的集合 + */ + public static List toBizPodList(List podList) { + return podList.parallelStream().map(obj -> toBizPod(obj)).collect(Collectors.toList()); + } + + /** + * 将Pod 转为 PodBiz,并设置completedTime + * + * @param pod Pod对象 + * @return BizPod BizPod对象 + */ + public static BizPod toBizPod(Pod pod) { + if (pod == null) { + return null; + } + BizPod bizPod = MappingUtils.mappingTo(pod, BizPod.class); + List containerStatus = pod.getStatus().getContainerStatuses(); + if (!CollectionUtil.isEmpty(containerStatus)) { + containerStatus.forEach(item -> { + if (item.getState().getTerminated() != null && COMPLETED.equals(item.getState().getTerminated().getReason())) { + bizPod.setCompletedTime(item.getState().getTerminated().getFinishedAt()); + } + }); + } + return bizPod; + } + /** + * 将List 转为 List + * + * @param deploymentList Deployment集合 + * @return List BizDeployment集合 + */ + public static List toBizDeploymentList(List deploymentList) { + return deploymentList.parallelStream().map(obj -> toBizDeployment(obj)).collect(Collectors.toList()); + } + /** + * 将Deployment 转为 BizDeployment + * + * @param deployment Deployment对象 + * @return BizDeployment BizDeployment对象 + */ + public static BizDeployment toBizDeployment(Deployment deployment) { + return MappingUtils.mappingTo(deployment, BizDeployment.class); + } + /** + * 将List 转为 List + * + * @param jobList Job的集合 + * @return List BizJob的集合 + */ + public static List toBizJobList(List jobList) { + return jobList.parallelStream().map(obj -> toBizJob(obj)).collect(Collectors.toList()); + } + /** + * 将Job 转为 BizJob + * + * @param job Job对象 + * @return BizJob BizJob对象 + */ + public static BizJob toBizJob(Job job) { + return MappingUtils.mappingTo(job, BizJob.class); + } + /** + * 将Namespace 转为 BizNamespace + * + * @param namespace 命名空间 + * @return BizNamespace BizNamespace命名空间 + */ + public static BizNamespace toBizNamespace(Namespace namespace) { + return MappingUtils.mappingTo(namespace, BizNamespace.class); + } + /** + * 将Node 转为 BizNode + * + * @param node Node对象 + * @return BizNode BizNode对象 + */ + public static BizNode toBizNode(Node node) { + return MappingUtils.mappingTo(node, BizNode.class); + } + + /** + * 将List 转为 List + * @param nodes + * @return List + */ + public static List toBizNodes(List nodes){ + List bizNodeList = new ArrayList<>(); + if (CollectionUtils.isEmpty(nodes)){ + return bizNodeList; + } + for (Node node : nodes){ + bizNodeList.add(toBizNode(node)); + } + return bizNodeList; + } + + /** + * 将PersistentVolumeClaim 转为 BizPersistentVolumeClaim + * + * @param persistentVolumeClaim 对象 + * @return BizPersistentVolumeClaim PersistentVolumeClaim实体类 + */ + public static BizPersistentVolumeClaim toBizPersistentVolumeClaim(PersistentVolumeClaim persistentVolumeClaim) { + return MappingUtils.mappingTo(persistentVolumeClaim, BizPersistentVolumeClaim.class); + } + + /** + * 将ResourceQuota 转为 BizResourceQuota + * + * @param resourceQuota 资源对象 + * @return BizResourceQuota resourceQuota业务类 + */ + public static BizResourceQuota toBizResourceQuota(ResourceQuota resourceQuota) { + return MappingUtils.mappingTo(resourceQuota, BizResourceQuota.class); + } + + /** + * 将LimitRange 转为 BizLimitRange + * + * @param limitRange 对象 + * @return BizLimitRange BizLimitRange实体类 + */ + public static BizLimitRange toBizLimitRange(LimitRange limitRange) { + return MappingUtils.mappingTo(limitRange, BizLimitRange.class); + } + + /** + * 将DistributeTrain 转为 BizDistributeTrain + * + * @param distributeTrain 对象 + * @return + */ + public static BizDistributeTrain toBizDistributeTrain(DistributeTrain distributeTrain){ + return MappingUtils.mappingTo(distributeTrain,BizDistributeTrain.class); + } + + /** + * 将List 转为 List + * + * @param distributeTrainList 对象 + * @return + */ + public static List toBizDistributeTrainList(List distributeTrainList){ + List distributeTrains = distributeTrainList.parallelStream().map(obj -> toBizDistributeTrain(obj)).collect(Collectors.toList()); + return distributeTrains; + } + + /** + * 将Service 转为 BizService + * + * @param service 对象 + * @return + */ + public static BizService toBizService(Service service) { + return MappingUtils.mappingTo(service, BizService.class); + } + + /** + * 将Secret 转为 BizSecret + * + * @param secret 对象 + * @return + */ + public static BizSecret toBizSecret(Secret secret) { + return MappingUtils.mappingTo(secret, BizSecret.class); + } + + /** + * 将Ingress 转为 BizIngress + * + * @param ingress 对象 + * @return + */ + public static BizIngress toBizIngress(Ingress ingress) { + BizIngress bizIngress = MappingUtils.mappingTo(ingress, BizIngress.class); + bizIngress.getRules().forEach(BizIngressRule::takeServicePort); + return bizIngress; + } + + /** + * 将Taint 转为 BizTaint对象 + * + * @param taint + * @return + */ + public static BizTaint toBizTaint(Taint taint){ + BizTaint bizTaint = MappingUtils.mappingTo(taint,BizTaint.class); + return bizTaint; + } + + /** + * 将BizTaint 转为 Taint + * + * @param bizTaint + * @return Taint + */ + public static Taint toTaint(BizTaint bizTaint){ + if (bizTaint == null){ + return null; + } + return new Taint(bizTaint.getEffect(),bizTaint.getKey(),bizTaint.getTimeAdded(),bizTaint.getValue()); + } + + /** + * 将 List 转为 List + * + * @param bizTaintList + * @return List + */ + public static List toTaints(List bizTaintList){ + List taintList = new ArrayList<>(); + if (CollectionUtils.isEmpty(bizTaintList)){ + return taintList; + } + for (BizTaint bizTaint : bizTaintList){ + Taint taint = toTaint(bizTaint); + if (taint != null){ + taintList.add(taint); + } + } + return taintList; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sCallBackTool.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sCallBackTool.java new file mode 100644 index 0000000..6f9db2d --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sCallBackTool.java @@ -0,0 +1,189 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.utils.AesUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.k8s.enums.BusinessLabelServiceNameEnum; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * @description k8s命名相关工具类 token使用MD5加密,并设置超时时间 + * @date 2020-05-28 + */ +@Component +public class K8sCallBackTool { + + /** + * k8s 回调 token秘钥 + */ + @Value("${k8s.callback.token.secret-key}") + private String secretKey; + /** + * k8s 回调token超时时间 + */ + @Value("${k8s.callback.token.expire-seconds}") + private Integer expireSeconds; + /** + * k8s 回调域名或IP:Port + */ + @Value("${k8s.callback.url}") + private String url; + + /** + * k8s 回调token key + */ + public static final String K8S_CALLBACK_TOKEN = AuthConst.K8S_CALLBACK_TOKEN; + /** + * 失败重试次数 + */ + private static final int RETRY_COUNT = 3; + + /** + * k8s 回调匹配地址 + */ + private static final List K8S_CALLBACK_PATH; + /** + * k8s 回调路径 + */ + private static final String K8S_CALLBACK_PATH_DEPLOYMENT = "/api/k8s/callback/deployment/"; + public static final String K8S_CALLBACK_PATH_POD = StringConstant.K8S_CALLBACK_URI+ SymbolConstant.SLASH; + + static { + K8S_CALLBACK_PATH = new ArrayList<>(); + // 添加需要token权限校验的地址(Shiro匿名访问的地址) + K8S_CALLBACK_PATH.add(K8S_CALLBACK_PATH_POD + "**"); + K8S_CALLBACK_PATH.add(K8S_CALLBACK_PATH_DEPLOYMENT + "**"); + } + + /** + * 获取 k8s 回调匹配地址 + * + * @return List + */ + public static List getK8sCallbackPaths() { + return new ArrayList<>(K8S_CALLBACK_PATH); + } + + + /** + * 生成k8s回调 + * + * @return String + */ + public String generateToken() { + String expireTime = DateUtil.format( + DateUtil.offset(new Date(), DateField.SECOND, expireSeconds), + DatePattern.PURE_DATETIME_PATTERN + ); + return AesUtil.encrypt(expireTime, secretKey); + } + + /** + * 验证token + * + * @param token + * @return boolean + */ + public boolean validateToken(String token) { + String expireTime = AesUtil.decrypt(token, secretKey); + if (StringUtils.isEmpty(expireTime)){ + return false; + } + String nowTime = DateUtil.format( + new Date(), + DatePattern.PURE_DATETIME_PATTERN + ); + return expireTime.compareTo(nowTime) > 0; + } + + + /** + * 判断当前是否可以再次重试 + * + * @param retryTimes 第n次试图重试 + * @return boolean + */ + public boolean continueRetry(int retryTimes) { + return retryTimes <= RETRY_COUNT; + } + + /** + * 获取回调地址 + * + * @param podLabel + * @return String + */ + public String getPodCallbackUrl(String podLabel) { + return "http://"+BusinessLabelServiceNameEnum.getServiceNameByBusinessLabel(podLabel) + K8S_CALLBACK_PATH_POD + podLabel; + } + + /** + * 获取回调地址 + * + * @param businessLabel + * @return String + */ + public String getDeploymentCallbackUrl(String businessLabel) { + return "http://"+BusinessLabelServiceNameEnum.getServiceNameByBusinessLabel(businessLabel) + K8S_CALLBACK_PATH_DEPLOYMENT + businessLabel; + } + + + /** + * 获取超时时间秒 + * + * @param timeoutSecond 超时秒数 + * @return Long + */ + public static Long getTimeoutSecondLong(int timeoutSecond) { + return Long.valueOf( + DateUtil.format( + DateUtil.offset( + new Date(), DateField.SECOND, timeoutSecond + ), + DatePattern.PURE_DATETIME_PATTERN + ) + ); + } + + /** + * 获取当前秒数 + * + * @return Long + */ + public static Long getCurrentSecondLong() { + return Long.valueOf( + DateUtil.format( + new Date(), + DatePattern.PURE_DATETIME_PATTERN + ) + ); + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sNameTool.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sNameTool.java new file mode 100644 index 0000000..a4a2d44 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sNameTool.java @@ -0,0 +1,319 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.RandomUtil; +import org.apache.commons.lang3.StringUtils; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.enums.BizEnum; +import org.dubhe.biz.file.enums.BizPathEnum; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.api.NamespaceApi; +import org.dubhe.k8s.config.K8sNameConfig; +import org.dubhe.k8s.domain.resource.BizNamespace; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Date; + +/** + * @description k8s命名相关工具类 + * @date 2020-05-13 + */ +@Component +public class K8sNameTool { + @Autowired + private K8sNameConfig k8sNameConfig; + /** + * 命名分隔符 + */ + private static final char SEPARATOR = '-'; + /** + * 文件分隔符 + */ + private static final String K8S_FILE_SEPARATOR = "/"; + /** + * 资源名称前缀 + */ + private static final String RESOURCE_NAME = "rn"; + /** + * 随机长度值 + */ + private static final int RANDOM_LENGTH = 4; + + @Resource + private NamespaceApi namespaceApi; + + /** + * 生成 ResourceName + * + * @param bizEnum 业务枚举 + * @param resourceInfo 资源备注信息(保证同业务下唯一并且命名规范) + * @return String + */ + public String generateResourceName(BizEnum bizEnum, String resourceInfo) { + return bizEnum.getBizCode() + SEPARATOR + RESOURCE_NAME + SEPARATOR + resourceInfo; + } + + /** + * 生成 Notebook的Namespace + * + * @param userId + * @return namespace + */ + public String generateNamespace(long userId) { + return this.k8sNameConfig.getNamespace() + SEPARATOR + userId; + } + + /** + * 从resourceName中获取资源信息 + * + * @param bizEnum 业务枚举 + * @param resourceName + * @return resourceInfo + */ + public String getResourceInfoFromResourceName(BizEnum bizEnum, String resourceName) { + if (StringUtils.isEmpty(resourceName) || !resourceName.contains(bizEnum.getBizCode() + SEPARATOR)) { + return null; + } + return resourceName.replace(bizEnum.getBizCode() + SEPARATOR + RESOURCE_NAME + SEPARATOR, SymbolConstant.BLANK); + } + + /** + * 从namespace 获取使用者ID + * + * @param namespace + * @return Long + */ + public Long getUserIdFromNamespace(String namespace) { + if (StringUtils.isEmpty(namespace) || !namespace.contains(this.k8sNameConfig.getNamespace() + SEPARATOR)) { + return null; + } + return Long.valueOf(namespace.replace(this.k8sNameConfig.getNamespace() + SEPARATOR, "")); + } + + + /** + * 生成业务模块相对路径 + * + * @param bizPathEnum 业务路径枚举 + * @return String 例如: /{biz}/{userId}/{YYYYMMDDhhmmssSSS+四位随机数}/ + */ + public String getPath(BizPathEnum bizPathEnum, long userId) { + if (bizPathEnum == null) { + return null; + } + return optimizationPath(K8S_FILE_SEPARATOR + + bizPathEnum.getBizPath() + + K8S_FILE_SEPARATOR + + userId + + K8S_FILE_SEPARATOR + + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_FORMAT) + RandomUtil.randomString(RANDOM_LENGTH) + + K8S_FILE_SEPARATOR); + } + + /** + * 生成业务模块预置路径 + * + * @param bizPathEnum 业务路径枚举 + * @return String 例如: /{biz}/{userId}/{YYYYMMDDhhmmssSSS+四位随机数}/ + */ + public String getPrePath(BizPathEnum bizPathEnum, long userId) { + if (bizPathEnum == null) { + return null; + } + return optimizationPath(K8S_FILE_SEPARATOR + + bizPathEnum.getBizPath() + + K8S_FILE_SEPARATOR + + StringConstant.COMMON + + K8S_FILE_SEPARATOR + + userId + + K8S_FILE_SEPARATOR + + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_FORMAT) + RandomUtil.randomString(RANDOM_LENGTH) + + K8S_FILE_SEPARATOR); + } + + + /** + * 去除根路径 + * + * @param path + * @return String + */ + public String removeRootPath(String path) { + if (StringUtils.isBlank(path) || !path.startsWith(k8sNameConfig.getNfsRootPath())) { + return path; + } + return optimizationPath(K8S_FILE_SEPARATOR + path.replace(k8sNameConfig.getNfsRootPath(), "")); + } + + /** + * 路径添加bucket + * + * @param path + * @return String + */ + public String appendBucket(String path) { + return optimizationPath(K8S_FILE_SEPARATOR + + k8sNameConfig.getFileBucket() + + K8S_FILE_SEPARATOR + + path); + } + + /** + * 路径添加时间戳随机数 + * + * @param path + * @return String + */ + public String appendTimeStampAndRandomNum(String path) { + return optimizationPath(path + + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_FORMAT) + RandomUtil.randomString(RANDOM_LENGTH) + + K8S_FILE_SEPARATOR); + } + + /** + * 路径根据业务转换 + * + * @param path + * @param sourceBizPathEnum 源业务 Path + * @param targetBizPathEnum 目标业务 Path + * @return String + */ + public String convertNfsPath(String path, BizPathEnum sourceBizPathEnum, BizPathEnum targetBizPathEnum) { + if (!validateBizPath(path, sourceBizPathEnum) || targetBizPathEnum == null) { + return path; + } + return optimizationPath(path.replace(K8S_FILE_SEPARATOR + sourceBizPathEnum.getBizPath() + K8S_FILE_SEPARATOR + , K8S_FILE_SEPARATOR + targetBizPathEnum.getBizPath() + K8S_FILE_SEPARATOR)); + } + + /** + * 获取绝对路径 + * + * @param path + * @return String + */ + public String getAbsolutePath(String path) { + if (StringUtils.isBlank(path)) { + return path; + } + return optimizationPath(k8sNameConfig.getNfsRootPath() + + K8S_FILE_SEPARATOR + + k8sNameConfig.getFileBucket() + + K8S_FILE_SEPARATOR + + path); + } + + /** + * 验证 path 是否是所属业务路径 + * + * @param path + * @param bizPathEnum + * @return boolean + */ + public boolean validateBizPath(String path, BizPathEnum bizPathEnum) { + return org.apache.commons.lang3.StringUtils.isNotBlank(path) + && bizPathEnum != null + && path.contains(bizPathEnum.getBizPath()) + && !path.contains(K8S_FILE_SEPARATOR + k8sNameConfig.getFileBucket() + K8S_FILE_SEPARATOR) + && !path.contains(K8S_FILE_SEPARATOR + k8sNameConfig.getNfsRootPath() + K8S_FILE_SEPARATOR); + } + + /** + * 路径优化 + * + * @param path + * @return String + */ + private String optimizationPath(String path) { + if (StringUtils.isBlank(path)) { + return path; + } + return path.replaceAll("///*", K8S_FILE_SEPARATOR); + } + + /** + * 获取k8s pod标签 + * + * @param bizEnum + * @return String + */ + public String getPodLabel(BizEnum bizEnum) { + return bizEnum == null ? null : bizEnum.getBizCode(); + } + + /** + * 获取数据集在镜像中路径 + * + * @return String + */ + public String getDatasetPath() { + return k8sNameConfig.getDatasetPath(); + } + + + /** + * 自送生成K8S名称,供K8S使用 + * + * @return String + */ + public String getK8sName() { + return DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_FORMAT) + + RandomUtil.randomString(RANDOM_LENGTH); + } + + /** + * @param curUser 当前用户 + * @return namespace k8s的命名空间 + */ + public String getNamespace(UserContext curUser) { + String namespaceStr = this.generateNamespace(curUser.getId()); + BizNamespace bizNamespace = namespaceApi.get(namespaceStr); + if (null == bizNamespace) { + BizNamespace namespace = namespaceApi.create(namespaceStr, null); + if (null == namespace || !namespace.isSuccess()) { + LogUtil.error(LogEnum.BIZ_K8S, "用户{} namespace为空", curUser.getUsername()); + } + } + return namespaceStr; + } + + /** + * @param userId 用户id + * @return namespace k8s的命名空间 + */ + public String getNamespace(Long userId) { + String namespaceStr = this.generateNamespace(userId); + BizNamespace bizNamespace = namespaceApi.get(namespaceStr); + if (null == bizNamespace) { + BizNamespace namespace = namespaceApi.create(namespaceStr, null); + if (null == namespace || !namespace.isSuccess()) { + LogUtil.error(LogEnum.BIZ_K8S, "用户{} namespace为空", userId); + } + } + return namespaceStr; + } + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sUtils.java new file mode 100644 index 0000000..557d643 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/K8sUtils.java @@ -0,0 +1,160 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import lombok.Getter; +import org.apache.commons.io.IOUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.k8s.constant.K8sLabelConstants; +import org.dubhe.k8s.properties.ClusterProperties; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.ClassPathResource; + +import java.io.File; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * @description init K8sUtils + * @date 2020-04-09 + */ +@Getter +public class K8sUtils implements ApplicationContextAware { + + /** + * K8S通信协议 + **/ + private final static String HTTPS_PREFIX = "https"; + + private final static String USER_DIR_SYSTEM_PROPERTY = "user.dir"; + + private ApplicationContext applicationContext; + + private KubernetesClient client; + private Config config; + private String nfs; + private String host; + private String port; + + /** + * 获取 k8s 连接配置文件 + * @param kubeConfig + * @return + * @throws IOException + */ + private String getKubeconfigFile(String kubeConfig) throws IOException { + kubeConfig = kubeConfig.startsWith(File.separator) ? kubeConfig : File.separator.concat(kubeConfig); + String path = System.getProperty(USER_DIR_SYSTEM_PROPERTY) + kubeConfig; + LogUtil.info(LogEnum.BIZ_K8S, "kubeconfig path:{}", path); + FileUtil.writeFromStream(new ClassPathResource(kubeConfig).getInputStream(), path); + return path; + } + + /** + * 构造 KubernetesClient + * @param clusterProperties + * @throws IOException + */ + public K8sUtils(ClusterProperties clusterProperties) throws IOException{ + String kubeConfig = clusterProperties.getKubeconfig(); + if (StrUtil.isNotBlank(kubeConfig)) { + String kubeConfigFile = getKubeconfigFile(kubeConfig); + //修改环境变量,重新指定kubeconfig读取位置 + System.setProperty(Config.KUBERNETES_KUBECONFIG_FILE, kubeConfigFile); + client = new DefaultKubernetesClient(); + config = client.getConfiguration(); + + } else { + LogUtil.warn(LogEnum.BIZ_K8S, "can't find kubeconfig in classpath, ignoring"); + String k8sUrl = clusterProperties.getUrl(); + if (k8sUrl.startsWith(HTTPS_PREFIX)) { + config = new ConfigBuilder().withMasterUrl(k8sUrl) + .withTrustCerts(true) + .withCaCertData(IOUtils.toString(clusterProperties.getCaCrt().getInputStream(), "UTF-8")) + .withClientCertData(Base64.getEncoder().encodeToString(IOUtils.toByteArray(clusterProperties.getClientCrt().getInputStream()))) + .withClientKeyData(IOUtils.toString(clusterProperties.getClientKey().getInputStream(), "UTF-8")) + .build(); + } else { + config = new ConfigBuilder().withMasterUrl(k8sUrl).build(); + } + LogUtil.info(LogEnum.BIZ_K8S, "config信息为{}", JSON.toJSONString(config)); + client = new DefaultKubernetesClient(config); + LogUtil.info(LogEnum.BIZ_K8S, "client为{}", JSON.toJSONString(client)); + } + + nfs = clusterProperties.getNfs(); + host = clusterProperties.getHost(); + port = clusterProperties.getPort(); + + //打印集群信息 + LogUtil.info(LogEnum.BIZ_K8S, "ApiVersion : {}", client.getApiVersion()); + LogUtil.info(LogEnum.BIZ_K8S, "MasterUrl : {}", client.getMasterUrl()); + LogUtil.info(LogEnum.BIZ_K8S, "NFS Server : {}", nfs); + LogUtil.info(LogEnum.BIZ_K8S, "VersionInfo : {}", JSON.toJSONString(client.getVersion())); + } + + /** + * 导出成yaml字符串 + * + * @param resource 任意对象 + * @return String 转换后的yaml字符串 + */ + public String convertToYaml(Object resource) { + return YamlUtils.convertToYaml(resource); + } + + /** + * 导出成yaml字符串 + * + * @param resource 任意对象 + * @param filePath 文件路径 + */ + public void dumpToYaml(Object resource, String filePath) { + YamlUtils.dumpToYaml(resource, filePath); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * 获取gpu选择label + * @param gpuNum + * @return + */ + public Map gpuSelector(Integer gpuNum) { + Map gpuSelector = new HashMap<>(2); + if (gpuNum != null && gpuNum > 0) { + gpuSelector.put(K8sLabelConstants.NODE_GPU_LABEL_KEY, K8sLabelConstants.NODE_GPU_LABEL_VALUE); + } + return gpuSelector; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/LabelUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/LabelUtils.java new file mode 100644 index 0000000..7ba42a8 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/LabelUtils.java @@ -0,0 +1,140 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import cn.hutool.core.util.ArrayUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.k8s.constant.K8sLabelConstants; + +import java.util.HashMap; +import java.util.Map; + +/** + * @description label 工具类 + * @date 2020-06-17 + */ +public class LabelUtils { + /** + * 生成基础标签集,包含资源名、创建者、环境 标签 + * + * @param resourceName 命名空间 + * @param labels 可变参数标签Map + * @return + */ + public static Map getBaseLabels(String resourceName, Map... labels) { + Map baseLabels = new HashMap<>(MagicNumConstant.SIXTEEN); + baseLabels.put(K8sLabelConstants.BASE_TAG_SOURCE, resourceName); + baseLabels.put(K8sLabelConstants.BASE_TAG_CREATE_BY, K8sLabelConstants.BASE_TAG_CREATE_BY_VALUE); + baseLabels.put(K8sLabelConstants.PLATFORM_RUNTIME_ENV, SpringContextHolder.getActiveProfile()); + if (ArrayUtil.isNotEmpty(labels)) { + for (Map obj : labels) { + if (obj != null) { + baseLabels.putAll(obj); + } + } + } + return baseLabels; + } + + /** + * 生成基础标签集,包含资源名、创建者、环境、业务 标签 + * + * @param resourceName 命名空间 + * @param business 业务标签 + * @param labels 可变参数标签Map + * @return + */ + public static Map getBaseLabels(String resourceName, String business, Map... labels) { + Map labelMap = getBaseLabels(resourceName, labels); + if (null != business) { + labelMap.put(K8sLabelConstants.BASE_TAG_BUSINESS, business); + } + return labelMap; + } + + /** + * 生成子资源标签集,包含资源名、创建者、环境、父资源、父类型 标签 + * + * @param resourceName 命名空间 + * @param pName 父资源名称 + * @param pKind 父类型名称 + * @param labels 可变参数标签Map + * @return + */ + public static Map getChildLabels(String resourceName, String pName, String pKind, Map... labels) { + Map baseLabels = getBaseLabels(resourceName, labels); + baseLabels.put(K8sLabelConstants.BASE_TAG_P_NAME, pName); + baseLabels.put(K8sLabelConstants.BASE_TAG_P_KIND, pKind); + return baseLabels; + } + + /** + * 生成子资源标签集,包含资源名、创建者、环境、父资源、父类型、业务标签 标签 + * + * @param resourceName 命名空间 + * @param pName 父资源名称 + * @param pKind 父类型名称 + * @param business 业务标签 + * @param labels 可变参数标签Map + * @return + */ + public static Map getChildLabels(String resourceName, String pName, String pKind, String business, Map... labels) { + Map labelMap = getChildLabels(resourceName, pName, pKind, labels); + if (null != business) { + labelMap.put(K8sLabelConstants.BASE_TAG_BUSINESS, business); + } + return labelMap; + } + + /** + * 添加环境标签用以分流 + * + * @param resourceName 资源名称 + * @return Map map + */ + public static Map withEnvResourceName(String resourceName) { + Map labels = withEnvResourceName(); + labels.put(K8sLabelConstants.BASE_TAG_SOURCE, resourceName); + return labels; + } + + /** + * 添加环境标签用以分流 + * + * @return Map map + */ + public static Map withEnvResourceName() { + Map labels = new HashMap<>(0); + labels.put(K8sLabelConstants.PLATFORM_RUNTIME_ENV, SpringContextHolder.getActiveProfile()); + return labels; + } + + /** + * 附带环境标签 + * + * @param key 标签健 + * @param value 标签值 + * @return Map map + */ + public static Map withEnvLabel(String key, String value) { + Map labels = withEnvResourceName(); + labels.put(key, value); + return labels; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/MappingUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/MappingUtils.java new file mode 100644 index 0000000..6704455 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/MappingUtils.java @@ -0,0 +1,153 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import cn.hutool.core.util.StrUtil; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.k8s.annotation.K8sField; +import org.dubhe.biz.log.utils.LogUtil; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @description 将 fabric8 pojo 转换为 resource下Biz类 + * 规则: 根据 ":" 分割 @K8sField,反射注入属性 + * 示例:@K8sField("spec:template:spec:containers") + * private List containers; 对应 Deployment.spec.template.containers + * @date 2020-04-17 + */ +public class MappingUtils { + + /** + * fabric pojo 转换为 biz pojo 注意output类和input类中 不要使用基本类型,应使用封装类型 + **/ + public static V mappingTo(T input, Class outputClass) { + //不判null的话,若T的属性有默认值,则会输出包含对应默认值的V实例 + if (null == input || outputClass == null) { + return null; + } + //相同基本类的封装类直接输出 比如 String + if (outputClass.isInstance(input)) { + return outputClass.cast(input); + } + + Field[] fields = outputClass.getDeclaredFields(); + V output = null; + try { + output = outputClass.newInstance(); + if (fields == null || fields.length < 1) { + return output; + } + + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + K8sField k8sField = field.getDeclaredAnnotation(K8sField.class); + if (k8sField == null) { + continue; + } + String expression = k8sField.value(); + String[] splitExpression = expression.split(SymbolConstant.COLON); + Object currentArg = input; + + int j = 0; + while (j < splitExpression.length) { + if (currentArg == null) { + break; + } + String funcName = SymbolConstant.GET + StrUtil.upperFirst(splitExpression[j]); + Method getMethod = currentArg.getClass().getDeclaredMethod(funcName, null); + currentArg = getMethod.invoke(currentArg, null); + j++; + } + + if (currentArg == null) { + continue; + } + + //List类型处理 + if (List.class.isAssignableFrom(field.getType()) + && field.getGenericType() instanceof ParameterizedType + && List.class.isAssignableFrom(currentArg.getClass())) { + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + //泛型里的类型 + Class actualTypeArgument = (Class) pt.getActualTypeArguments()[0]; + List outputFieldList = new ArrayList(); + List inputArgList = ((List) currentArg); + for (Object inputArg : inputArgList) { + Object outputElm = mappingTo(inputArg, actualTypeArgument); + outputFieldList.add(outputElm); + } + currentArg = outputFieldList; + } else if (Map.class.isAssignableFrom(field.getType()) && field.getGenericType() instanceof ParameterizedType + && Map.class.isAssignableFrom(currentArg.getClass())) { + /**处理map value类型不同**/ + Map map = (Map) currentArg; + map.forEach((key, value) -> { + map.put(mappingTo(key, getMapValueClass(field, 0)), mappingTo(value, getMapValueClass(field, 1))); + }); + } else if (!field.getType().isInstance(currentArg) && field.getType().getDeclaredFields().length > 0) { + //递归处理属性 + currentArg = mappingTo(currentArg, field.getType()); + } + + //如果类型相同或是field的子类,赋值 + String setFuncName = SymbolConstant.SET + StrUtil.upperFirst(field.getName()); + Method setMethod = outputClass.getDeclaredMethod(setFuncName, field.getType()); + setMethod.invoke(output, currentArg); + } + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_K8S, "MappingUtils.mappingTo error, message{} ",e.getMessage(), e); + throw new RuntimeException("fabric to biz failed"); + } + return output; + } + + /** + * 获取map 的key或value 的类型 + * + * @param field 反射字段 + * @param fieldIndex 0获取key的Class 1获取Value的Class + * @return Class Class类对象 + */ + private static Class getMapValueClass(Field field, int fieldIndex) { + try { + if (Map.class.isAssignableFrom(field.getType())) { + Type mapMainType = field.getGenericType(); + if (mapMainType instanceof ParameterizedType) { + // 执行强制类型转换 + ParameterizedType parameterizedType = (ParameterizedType) mapMainType; + // 获取泛型类型的泛型参数 + Type[] types = parameterizedType.getActualTypeArguments(); + return Class.forName(types[fieldIndex].getTypeName()); + } else { + LogUtil.error(LogEnum.BIZ_K8S, "Error getting generic type {}",mapMainType.getTypeName()); + } + } + } catch (ClassNotFoundException e) { + LogUtil.error(LogEnum.BIZ_K8S, "MappingUtils.getMapValueClass error, message {}", e.toString(),e); + } + return null; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PodUtil.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PodUtil.java new file mode 100644 index 0000000..02fdff1 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PodUtil.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.utils; + +/** + * @description k8s Pod 工具类 + * @date 2020-09-17 + */ +public class PodUtil { + + private PodUtil(){ + + } + + /** + * 判断节点是否是master节点 + * @param podName k8s pod名称 + * @return true master节点,false 其他 + */ + public static boolean isMaster(String podName){ + if(podName == null){ + return false; + } + return podName.contains("-master-"); + } + + /** + * 判断节点是否是slave节点 + * @param podName k8s pod名称 + * @return true slave节点,false 其他 + */ + public static boolean isSlave(String podName){ + if(podName == null){ + return false; + } + return podName.contains("-slave-"); + } + + +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PrometheusUtil.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PrometheusUtil.java new file mode 100644 index 0000000..d0df8f2 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/PrometheusUtil.java @@ -0,0 +1,102 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.k8s.utils; + +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import org.apache.commons.collections4.map.HashedMap; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.bo.PrometheusMetricBO; +import org.dubhe.k8s.domain.dto.PodQueryDTO; + +import java.util.Map; + +/** + * @description prometheus 工具类 + * @date 2021-02-03 + */ +public class PrometheusUtil { + /** + * prometheus get查询 + * @param url + * @param paramMap + * @return + */ + public static PrometheusMetricBO getQuery(String url,Map paramMap){ + if (StringUtils.isEmpty(url)){ + return null; + } + try { + String metricStr = HttpUtil.get(url,paramMap); + if (StringUtils.isEmpty(metricStr)){ + return null; + } + return JSON.parseObject(metricStr, PrometheusMetricBO.class); + }catch (Exception e){ + LogUtil.error(LogEnum.BIZ_K8S, "getQuery url:{} paramMap:{} error:{}", url,paramMap,e.getMessage(),e); + return null; + } + } + + /** + * 组装参数 + * @param param 查询表达式 + * @param podName pod名称 + * @param podQueryDTO 查询参数 + * @return + */ + public static Map getQueryParamMap(String param,String podName,PodQueryDTO podQueryDTO){ + Map paramMap = new HashedMap<>(MagicNumConstant.EIGHT); + if (StringUtils.isEmpty(param) || StringUtils.isEmpty(podName)){ + return paramMap; + } + paramMap.put(StringConstant.QUERY, param.replace(K8sParamConstants.POD_NAME_PLACEHOLDER,podName)); + if (podQueryDTO == null){ + return paramMap; + } + if (podQueryDTO.getStartTime() != null){ + paramMap.put(StringConstant.START_LOW,podQueryDTO.getStartTime()); + } + if (podQueryDTO.getEndTime() != null){ + paramMap.put(StringConstant.END_LOW,podQueryDTO.getEndTime()); + } + if (podQueryDTO.getStep() != null){ + paramMap.put(StringConstant.STEP_LOW,podQueryDTO.getStep()); + } + return paramMap; + } + + /** + * 组装参数 + * @param param 查询表达式 + * @param podName pod名称 + * @return + */ + public static Map getQueryParamMap(String param,String podName){ + Map paramMap = new HashedMap<>(MagicNumConstant.TWO); + if (StringUtils.isEmpty(param) || StringUtils.isEmpty(podName)){ + return paramMap; + } + paramMap.put(StringConstant.QUERY, param.replace(K8sParamConstants.POD_NAME_PLACEHOLDER,podName)); + return paramMap; + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ResourceBuildUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ResourceBuildUtils.java new file mode 100644 index 0000000..b74b630 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ResourceBuildUtils.java @@ -0,0 +1,205 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.Toleration; +import io.fabric8.kubernetes.api.model.extensions.Ingress; +import io.fabric8.kubernetes.api.model.extensions.IngressBuilder; +import io.fabric8.kubernetes.api.model.extensions.IngressRule; +import io.fabric8.kubernetes.api.model.extensions.IngressRuleBuilder; +import io.fabric8.kubernetes.api.model.extensions.IngressTLS; +import io.fabric8.kubernetes.api.model.extensions.IngressTLSBuilder; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.k8s.domain.bo.BuildIngressBO; +import org.dubhe.k8s.domain.bo.BuildServiceBO; +import org.dubhe.k8s.enums.K8sTolerationEffectEnum; +import org.dubhe.k8s.enums.K8sTolerationOperatorEnum; + +import java.util.Map; + +/** + * @description 构建 Kubernetes 资源对象 + * @date 2020-09-10 + */ +public class ResourceBuildUtils { + + /** + * 构建 Service + * @param bo + * @return + */ + public static Service buildService(BuildServiceBO bo) { + return new ServiceBuilder() + .withNewMetadata() + .withName(bo.getName()) + .addToLabels(bo.getLabels()) + .withNamespace(bo.getNamespace()) + .endMetadata() + .withNewSpec() + .withPorts(bo.getPorts()) + .withSelector(bo.getSelector()) + .endSpec() + .build(); + } + + /** + * 构建 ServicePort + * @param targetPort + * @param port + * @param name + * @return + */ + public static ServicePort buildServicePort(Integer targetPort, Integer port, String name){ + ServicePort servicePort = new ServicePortBuilder() + .withNewTargetPort(targetPort) + .withPort(port) + .withName(name) + .build(); + return servicePort; + } + + /** + * 构建 IngressRule + * @param host + * @param serviceName + * @param servicePort + * @return + */ + public static IngressRule buildIngressRule(String host, String serviceName, Integer servicePort){ + return new IngressRuleBuilder() + .withHost(host) + .withNewHttp() + .addNewPath() + .withPath(SymbolConstant.SLASH) + .withNewBackend() + .withNewServiceName(serviceName) + .withNewServicePort(servicePort) + .endBackend() + .endPath() + .endHttp() + .build(); + } + + /** + * 构建 IngressRule + * @param host + * @param serviceName + * @param servicePort + * @return + */ + public static IngressRule buildIngressRule(String host, String serviceName, String servicePort){ + return new IngressRuleBuilder() + .withHost(host) + .withNewHttp() + .addNewPath() + .withPath(SymbolConstant.SLASH) + .withNewBackend() + .withNewServiceName(serviceName) + .withNewServicePort(servicePort) + .endBackend() + .endPath() + .endHttp() + .build(); + } + + /** + * 构建 IngressTLS + * @param secretName + * @param host + * @return + */ + public static IngressTLS buildIngressTLS(String secretName, String host){ + return new IngressTLSBuilder() + .withSecretName(secretName) + .withHosts(host) + .build(); + } + + /** + * 构建 Ingress + * @param bo + * @return + */ + public static Ingress buildIngress(BuildIngressBO bo) { + return new IngressBuilder() + .withNewMetadata() + .withName(bo.getName()) + .addToLabels(bo.getLabels()) + .withNamespace(bo.getNamespace()) + .addToAnnotations(bo.getAnnotations()) + .endMetadata() + .withNewSpec() + .withRules(bo.getIngressRules()) + .withTls(bo.getIngressTLSs()) + .endSpec() + .build(); + } + + /** + * 构建 Secret + * @param namespace + * @param name + * @param labels + * @param map + * @return + */ + public static Secret buildTlsSecret(String namespace, String name, Map labels, Map map){ + Secret secret = new SecretBuilder() + .withType(K8sParamConstants.SECRET_TLS_TYPE) + .withNewMetadata() + .withName(name) + .addToLabels(labels) + .withNamespace(namespace) + .endMetadata() + .addToData(map) + .build(); + return secret; + } + + /** + * 构建 Toleration + * + * @param effect + * @param key + * @param operator + * @param tolerationSeconds + * @param value + * @return + */ + public static Toleration buildToleration(String effect,String key,String operator,Long tolerationSeconds,String value){ + return new Toleration(effect,key,operator,tolerationSeconds,value); + } + + /** + * 构建 effect=NoSchedule operator=Equal 的 Toleration + * + * @param key 键 + * @param value 值 + * @return + */ + public static Toleration buildNoScheduleEqualToleration(String key,String value){ + return new Toleration(K8sTolerationEffectEnum.NOSCHEDULE.getEffect(),key, K8sTolerationOperatorEnum.EQUAL.getOperator(),null,value); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/UnitConvertUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/UnitConvertUtils.java new file mode 100644 index 0000000..c2cf706 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/UnitConvertUtils.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.RegexUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.k8s.constant.K8sParamConstants; + +/** + * @description k8s 资源单位转换 + * @date 2020-10-13 + */ +public class UnitConvertUtils { + + /** + * cpu转化为 n + * @param amount 值 + * @param format 单位 + * @return + */ + public static Long cpuFormatToN(String amount,String format){ + if (StringUtils.isEmpty(amount) || !RegexUtil.isDigits(amount)){ + return MagicNumConstant.ZERO_LONG; + } + if (StringUtils.isEmpty(format)){ + return Long.valueOf(amount) * MagicNumConstant.ONE_THOUSAND * MagicNumConstant.MILLION; + } + if (K8sParamConstants.CPU_UNIT.equals(format)){ + return Long.valueOf(amount) * MagicNumConstant.MILLION; + } + return Long.valueOf(amount); + } + + /** + * 内存转为 Mi + * @param amount 值 + * @param format 单位 + * @return + */ + public static Long memFormatToMi(String amount,String format){ + if (StringUtils.isEmpty(amount) || !RegexUtil.isDigits(amount)){ + return MagicNumConstant.ZERO_LONG; + } + if (K8sParamConstants.MEM_UNIT_TI.equals(format)){ + return Long.valueOf(amount) * MagicNumConstant.BINARY_TEN_EXP * MagicNumConstant.BINARY_TEN_EXP; + } + if (K8sParamConstants.MEM_UNIT_GI.equals(format)){ + return Long.valueOf(amount) * MagicNumConstant.BINARY_TEN_EXP; + } + if (K8sParamConstants.MEM_UNIT_KI.equals(format)){ + return Long.valueOf(amount) / MagicNumConstant.BINARY_TEN_EXP; + } + return Long.valueOf(amount); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ValidationUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ValidationUtils.java new file mode 100644 index 0000000..5fba7d6 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/ValidationUtils.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import org.dubhe.k8s.constant.K8sParamConstants; +import org.dubhe.biz.base.utils.StringUtils; + +/** + * @description 校验工具类 + * @date 2020-06-05 + */ +public class ValidationUtils { + /** + * 校验resourceName是否符合k8s命名规范 + * + * @param resourceName 资源名称 + * @return boolean true 成功 false 不成功 + */ + public static boolean validateResourceName(String resourceName) { + return !(StringUtils.isEmpty(resourceName) || resourceName.length() > K8sParamConstants.RESOURCE_NAME_LENGTH || !resourceName.matches(K8sParamConstants.K8S_RESOURCE_NAME_REGEX)); + } +} diff --git a/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/YamlUtils.java b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/YamlUtils.java new file mode 100644 index 0000000..96232cf --- /dev/null +++ b/dubhe-server/common-k8s/src/main/java/org/dubhe/k8s/utils/YamlUtils.java @@ -0,0 +1,120 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.k8s.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableList; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.internal.SerializationUtils; +import org.dubhe.biz.base.constant.SymbolConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; + +/** + * @description yaml基本操作工具类 + * @date 2020-04-15 + */ +public class YamlUtils { + + /** + * 导出yaml时无论有没有值都忽略的属性 + **/ + private final static ImmutableList IGNORE_PROPERTIES_WHEN_DUMP_YAML = ImmutableList.of("resourceVersion"); + + private static Yaml yaml; + + static { + initYaml(); + } + + /** + * 初始化Yaml + */ + private static void initYaml() { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + + yaml = new Yaml(new Representer() { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { + // 如果属性为null空数组,忽略它 + boolean beIgnored = (propertyValue instanceof Collection && ((Collection) propertyValue).size() == 0); + if (propertyValue == null || beIgnored) { + return null; + } else { + if (IGNORE_PROPERTIES_WHEN_DUMP_YAML.contains(property.getName())) { + return null; + } + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }, options); + } + + /** + * javabean转yaml字符串 + * + * @param resource 对象 + * @return String + */ + public static String convertToYaml(Object resource) { + return yaml.dump(resource); + } + + /** + * javabean导出成yaml文件 + * + * @param resource 资源名称 + * @param filePath 文件路径 + * @return void + */ + public static void dumpToYaml(Object resource, String filePath) { + try (Writer writer = new FileWriter(filePath)) { + yaml.dump(resource, writer); + } catch (IOException e) { + LogUtil.error(LogEnum.BIZ_K8S, "dumpToYaml error:{}", e); + } + } + + /** + * 转换成Yml格式字符串 + * + * @param obj KubernetesResource对象 + * @return yml格式字符串 + */ + public static String dumpAsYaml(HasMetadata obj) { + try { + return SerializationUtils.dumpAsYaml(obj); + } catch (JsonProcessingException e) { + LogUtil.error(LogEnum.BIZ_K8S, "dumpAsYamle error:{}", e); + } + return SymbolConstant.BLANK; + } +} diff --git a/dubhe-server/common-k8s/src/main/resources/kubeconfig_test b/dubhe-server/common-k8s/src/main/resources/kubeconfig_test new file mode 100644 index 0000000..9b9f6e5 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/resources/kubeconfig_test @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeE1EVXhOekF4TXpJd05Gb1hEVE14TURVeE5UQXhNekl3TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTHNEClVhOHJBQ3o3SHZETHN0UGhqZFBwWDJ2b1Vlb3hobFcraVdLTDNZanpIcFZVdWhOQktFNzlrZUNuZXhUMkxuUkgKanF2SGJTajZNMkpVbys3dyttWDQ4R3FpaTRRRENtTy9OWXJ6NGVpdTFTTEJsWmJlWWtBeWVYVDRwQVN3UFFTWQozY3FjUVh3N1pXY2hUNkl0R1kySHZPSWRKeFZvOTJEL2lxQUZwT0grR2Focld3Wm9wZWMwREpXaEE2RlZCOVE3CkpPeXRJQVE2alpjc2ZrQm9DdVRUOExqVXhya1cxSGRVTkFFOS9PTjVhNlhPQzF2b3Bia01aTE5uU2tjK2lDQ2EKK1M2WkI5b3VvWGNOOW5XN1NTdThhNHZDL2VhZU9WZGR6bFlrVHRnTC9LTm1DSVFkZEVVQmswVG5EUHhKeVhHTwpqUXVwWFVUYTByOXYxV21pVzFrQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFDQnVYb0F1M2tLbkNCWlhkbmd1cVNHRHRoaWUKVHRPSmJFcHBxUWo0SG43Sm9pcnVkVmdDMXQvWU8zVDZkRmxqL1ZLRFl3dXB0cFVmTHVWVTRWTVJmNHFCU3lQaAovb3ZLM285YkoxU1hjWVllenJ0dkFwUWNnQmdEVjcwQ0xlRHRkcnI1c0VWaUFvcklSdSsyT3Z6Vm85U2dCTFN4CmFMZTVKZEU0Z3NNYndTaUkxRlkza0E3OHlEcHU4THZVZFE2MWRvSVR4dE1VWE9HTkpVMkw2cVliRkxUeDZGaDIKWDkwOGIrMmRIZ0ZvSlk0OXVNaVdLUHAvbzlXU3pPQnphQ3lEK0UzbFBYdXFrakNZRzRodVg5dkExRXZIZ3ZOMwpuK2NyWTYzNWVKWGtVcjhIRFo2Rm1qeWtBUUFNUkFickJJYTJtY0laL3B2V1hYRW9yY1o3TjV2WXlIaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + server: https://127.0.0.1:6443 + name: kubernetes +contexts: +- context: + cluster: kubernetes + user: kubernetes-admin + name: kubernetes-admin@kubernetes +current-context: kubernetes-admin@kubernetes +kind: Config +preferences: {} +users: +- name: kubernetes-admin + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJQll2OGYvY21KUzh3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TVRBMU1UY3dNVE15TURSYUZ3MHlNakExTVRjd01UTXlNRGRhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdqZTQ2MXNiMGo5RDlQWG8KK3NleTg3bCtFZXNXRjRQTzk1ZXVGWmkwcGozMmx1MVJuZm9XbFhYSDVDNzhKalFQdXN0RU5pcmU3RTJyMEpJYgphWVBQSmpubVNNMXBLSXNnejl4bmNvTDFpSTdlK3RxQXBjcXRVcENwQVhoZ0YvbnlUNnZCT1BBaVMyNjdKcDBpCjBZU1VxMkc2TFovNlowWk9aT2ppb0NZOEN2U0ZJbjU1RUROUmUvL1U0QTV2SjgzVW1YMHRxTW1aUWhNZUtYaGYKa1NCWGRrZmlBcXVSNDNOVWxBcDNwT3NRUmxBU1ExRUNTek1yblE5MHNzYlRCWnF3djVXSCtKREZ2aTZLVWtTcwpxYmtUbWFvWEoyaS9hT1BtTWJJNUZNVm1zcWwvRk1mTXJ1K0IrZU1vUkpPeE53Nmx0VnlGOUJaaTNjZ29aQzlQClQrdWp4UUlEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKcXU0OXdXWkF4QzNxRCtiYVA0TE1maHFpZWNJeXZkSEc2YwpNU3RaUzNpZmt3a0JYNEN4QnJFaWh2a051TEpGNHI4dS9aT1Q0SStYVFpMUG8xTFYzRlNadU9Hc0JiYXJJVHpLCnA5a3ZaQ0tER1ltTng0OVg4aDEvcFpqS3VDWTNUSUF4VTE1VDZmK2pXOVhKa3JNRzFOeDdRVytySWs1VWpKa2YKemlTb2ZNY3VsM3QxOGMyQUUwYitob2RqNHVhMEd4N2U1ZkxwNjd3YUxERHZNdVQwTHNEelZkWWdGK2M1UWhmNQpBa1RsVFRqWGtZckRhbUpPKzRZN080UTd6NkpEUnNVK1ZiTU53S0NocCtzNGYzeS82SkdGSnZYaHgrNVZoRURZCnJ6TDZTTXIwR3M0MG9sNXNSUUNKZHpUUFA4dDZtdGdZVmlLeXJPeFlNcm5CTUtNN1VvRT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBd2plNDYxc2IwajlEOVBYbytzZXk4N2wrRWVzV0Y0UE85NWV1RlppMHBqMzJsdTFSCm5mb1dsWFhINUM3OEpqUVB1c3RFTmlyZTdFMnIwSkliYVlQUEpqbm1TTTFwS0lzZ3o5eG5jb0wxaUk3ZSt0cUEKcGNxdFVwQ3BBWGhnRi9ueVQ2dkJPUEFpUzI2N0pwMGkwWVNVcTJHNkxaLzZaMFpPWk9qaW9DWThDdlNGSW41NQpFRE5SZS8vVTRBNXZKODNVbVgwdHFNbVpRaE1lS1hoZmtTQlhka2ZpQXF1UjQzTlVsQXAzcE9zUVJsQVNRMUVDClN6TXJuUTkwc3NiVEJacXd2NVdIK0pERnZpNktVa1NzcWJrVG1hb1hKMmkvYU9QbU1iSTVGTVZtc3FsL0ZNZk0KcnUrQitlTW9SSk94Tnc2bHRWeUY5QlppM2Nnb1pDOVBUK3VqeFFJREFRQUJBb0lCQUNJZVdkejJ0Mjk2Nzd4RAp5dmJyU0JPcTNXdldhWjRkNktqME8zL053TWFIa2g4M2Q2UVNBQStuamtNV3dmTVFLRWMvV0M5UDNyT1NmWUY1CmVWbFM3M3dlcGNiYVZ3UHBWUTFQQWRsTENrbEFHQW5uZ3J3ZFc4OXFYRlpHeUZMTjlQUnNEdGlxenN1RG0xc1EKTmNLcTBOYytwczlIRUYwK0s1MXNrQXRrVEIzOFFtQjVyTDlCU0UyMkpHQytqUU1IMnk1Y2FaaGlocXRtSEg2OQpyN012WUFlamFDNVlGVDY0L2pMeFNncjM1cUlhKzBqdWxheExyT2xtYURXa2Nva0xieThPN0JUSTRkTmh5bHluCmg4ZWhYUCtvUVkzckJmR21GZG1OZTBNM0R3UGF2TkF1VVRQNkhMQTBOdXczZ0ZaYjdhU0tJU1Q1QWxiaFlzT24KcEZDTjFLRUNnWUVBMjFCY0U5elhqQVdWeWdnVmJYZ3pDRE9wL0pwRktYSE9LNHZVb2RHNHIwSGliUXBWUkJkMQpWSkxRVmQyZW1SSHRlczlmbjFvdnJkZFRWYUpzNXhWWUxiZmR2V2wyYXBnc2UyL1FDRGZ6Y3IzamhNQlNla1B2CjFJd01DOFdHeElWTG85N1VwUkxET0U0cnlxTDJzaUJLbEhhYXdaV1BuVlAzYnorenFXNUtGbTBDZ1lFQTRyU3IKdlpFbDdSdm5wbWhJdjlWQXNjeUtkYUxmRnpTdzFuMnBITWs3UDF6bWJaMldWR2U1MldldHNQNCtrOEZTNG04SQpIc1QwOTMwM2o0SkZ4YWp0b2dtOU1RRnE3TDMvY0tVdEpSTVd6QW95dm5oVVBvRE8vYXBKL2VNeHIrdTdkREhFClFFTlAzMVVjT1daU3lKSHNlRHNNY1JKby9iVlFoUlozS0ROZHk3a0NnWUE3MEZUc2plU3pxYXBLcVoyK2QzUGoKbnNPVHd6ZHRzRDQ4bml4bDNkN3kzWk0xamdYblJrYVh4RnJSc0ZuYkFZcTFYZTJFZG9KZWRVV2pLMk5zT3VRTAp4QVBUN3ZsKzVQWHN6SGYrWmRRZHpUQktPbkhFS3RjMEx1WHlKL016a2U4cFNGTFNtcVZucTlwQnIrUjhmRllhCjI2WWxlZmJyUDhWU01CdDk4RGlBbVFLQmdBVWI5MGJsYjRwaGQ1NExlYUJCS1IwWXRBSWtzb3h1VnBIdThSSEMKQTBEUlVpd2tRaEFTNm1CWTh0UXJWck96eHE5dHV5d2VXanI5cW5Qa2hyZ0dyNXhZUmRoRjVPZ0MvQy9JdVRTOQpzbVRVMGdIeTZrc2lVZ2ZyZjVGbVBtZHRrNkx4d0MrR2xOVStzTTBtWGpWQS9LaFZCRm5FQlhPNlUxODhlMkQvCmoxeVpBb0dBVmdZd0ZSRTBDUW81YUdFZXNLWDYvWVNEcHJZMjBLTzJROTV4RUVTRms0Ujl0WDI4TjhRcitBQ0EKUzI2eWZrY21vSjNXb3lGYVBSTlBUd0xvRVcyc0dVWmdCTWF4SzRHcy9HeUtXMzk4aGRmR0h2VGFwamNac0FDcgppeTFGams4dkwybGFrSTlMN0hzRjY0bHhCaG5QQmJtY3pMQW10bkZFNjY4cWhoMXc5cnM9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== \ No newline at end of file diff --git a/dubhe-server/common-k8s/src/main/resources/mapper/K8sTaskMapper.xml b/dubhe-server/common-k8s/src/main/resources/mapper/K8sTaskMapper.xml new file mode 100644 index 0000000..89e8bc8 --- /dev/null +++ b/dubhe-server/common-k8s/src/main/resources/mapper/K8sTaskMapper.xml @@ -0,0 +1,200 @@ + + + + + + id,namespace,resource_name,task_yaml,business,apply_unix_time,apply_display_time,apply_status, + stop_unix_time,stop_display_time,stop_status,create_time,create_user_id,update_time,update_user_id,deleted + + + + + insert into k8s_task + + + namespace, + + + resource_name, + + + task_yaml, + + + business, + + + apply_unix_time, + + + apply_display_time, + + + apply_status, + + + stop_unix_time, + + + stop_display_time, + + + stop_status, + + + create_time, + + + create_user_id, + + + update_time, + + + update_user_id, + + + deleted, + + + + + #{namespace}, + + + #{resourceName}, + + + #{taskYaml}, + + + #{business}, + + + #{applyUnixTime}, + + + #{applyDisplayTime}, + + + #{applyStatus}, + + + #{stopUnixTime}, + + + #{stopDisplayTime}, + + + #{stopStatus}, + + + #{createTime}, + + + #{createUserId}, + + + #{updateTime}, + + + #{updateUserId}, + + + #{deleted}, + + + ON DUPLICATE KEY UPDATE + + + task_yaml = #{taskYaml}, + + + business = #{business}, + + + apply_unix_time = #{applyUnixTime}, + + + apply_display_time = #{applyDisplayTime}, + + + apply_status = #{applyStatus}, + + + stop_unix_time = #{stopUnixTime}, + + + stop_display_time = #{stopDisplayTime}, + + + stop_status = #{stopStatus}, + + + create_time = #{createTime}, + + + create_user_id = #{createUserId}, + + + update_time = #{updateTime}, + + + update_user_id = #{updateUserId}, + + + deleted = #{deleted}, + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-recycle/pom.xml b/dubhe-server/common-recycle/pom.xml new file mode 100644 index 0000000..e4c3631 --- /dev/null +++ b/dubhe-server/common-recycle/pom.xml @@ -0,0 +1,49 @@ + + + + server + org.dubhe + 0.0.1-SNAPSHOT + + 4.0.0 + common-recycle + 0.0.1-SNAPSHOT + 垃圾回收 recycle 通用模块 + Dubhe recycle Common + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + org.dubhe.biz + file + ${org.dubhe.biz.file.version} + + + org.dubhe.biz + data-permission + ${org.dubhe.biz.data-permission.version} + + + org.dubhe.biz + redis + ${org.dubhe.biz.redis.version} + + + + org.dubhe.biz + data-response + ${org.dubhe.biz.data-response.version} + + + + + + + + \ No newline at end of file diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleConfig.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleConfig.java new file mode 100644 index 0000000..b698b81 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleConfig.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @description 垃圾回收机制配置常量 + * @date 2020-09-21 + */ +@Data +@Component +@ConfigurationProperties(prefix = "recycle.timeout") +public class RecycleConfig { + + /** + * 回收无效文件的默认有效时长 + */ + private Integer date; + + /** + * 用户上传文件至临时路径下后文件最大有效时长,以小时为单位 + */ + private Integer fileValid; + + /** + * 用户删除某一算法后,其算法文件最大有效时长,以天为单位 + */ + private Integer algorithmValid; + + /** + * 用户删除某一模型后,其模型文件最大有效时长,以天为单位 + */ + private Integer modelValid; + + /** + * 用户删除训练任务后,其训练管理文件最大有效时长,以天为单位 + */ + private Integer trainValid; + + /** + * 用户删除度量文件后,其度量文件最大有效时长,以天为单位 + */ + private Integer measureValid; + + /** + * 用户删除镜像后,其镜像最大有效时长,以天为单位 + */ + private Integer imageValid; + + /** + * 回收serving相关文件后,回收文件最大有效时长,以天为单位 + */ + private Integer servingValid; + +} \ No newline at end of file diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleMvcConfig.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleMvcConfig.java new file mode 100644 index 0000000..6ba1e0d --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/config/RecycleMvcConfig.java @@ -0,0 +1,47 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.recycle.config; + + +import org.dubhe.recycle.interceptor.RecycleCallInterceptor; +import org.dubhe.recycle.utils.RecycleTool; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; + +/** + * @description 资源回收 Mvc Config + * @date 2021-01-21 + */ +@Configuration +public class RecycleMvcConfig implements WebMvcConfigurer { + + @Resource + private RecycleCallInterceptor recycleCallInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + InterceptorRegistration registration = registry.addInterceptor(recycleCallInterceptor); + // 拦截配置 + registration.addPathPatterns(RecycleTool.MATCH_RECYCLE_PATH); + } + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleDetailMapper.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleDetailMapper.java new file mode 100644 index 0000000..9a801f3 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleDetailMapper.java @@ -0,0 +1,27 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.dubhe.recycle.domain.entity.RecycleDetail; + +/** + * @description 回收垃圾详情 mapper + * @date 2021-02-03 + */ +public interface RecycleDetailMapper extends BaseMapper { +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleMapper.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleMapper.java new file mode 100644 index 0000000..cbbbe74 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/dao/RecycleMapper.java @@ -0,0 +1,27 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.dubhe.recycle.domain.entity.Recycle; + +/** + * @description 回收垃圾任务 mapper + * @date 2021-02-03 + */ +public interface RecycleMapper extends BaseMapper { +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleCreateDTO.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleCreateDTO.java new file mode 100644 index 0000000..678bab9 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleCreateDTO.java @@ -0,0 +1,111 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; +import org.dubhe.recycle.domain.entity.Recycle; +import org.dubhe.recycle.domain.entity.RecycleDetail; +import org.springframework.beans.BeanUtils; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * @description 创建垃圾回收任务DTO + * @date 2021-02-03 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class RecycleCreateDTO extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + @ApiModelProperty(value = "回收模块", required = true) + @NotNull(message = "回收模块不能为空") + private Integer recycleModule; + + @ApiModelProperty(value = "回收延迟时间,以天为单位") + private Integer recycleDelayDate; + + @ApiModelProperty(value = "回收定制化方式") + private String recycleCustom; + + @ApiModelProperty(value = "回收说明") + private String recycleNote; + + @ApiModelProperty(value = "回收备注") + private String remark; + + @ApiModelProperty(value = "还原定制化方式") + private String restoreCustom; + + @ApiModelProperty(value = "回收任务详情") + @NotEmpty(message = "回收任务详情不能为空") + private List detailList; + + /** + * 添加 RecycleDetailCreateDTO + * @param detailCreateDTO RecycleDetailCreateDTO + */ + public void addRecycleDetailCreateDTO(RecycleDetailCreateDTO detailCreateDTO){ + if (detailList == null){ + detailList = new ArrayList<>(); + } + detailList.add(detailCreateDTO); + } + + /** + * 创建 RecycleCreateDTO + * @param recycle 回收任务 + * @return RecycleCreateDTO + */ + public static RecycleCreateDTO recycleTaskCreateDTO(Recycle recycle){ + RecycleCreateDTO instance = new RecycleCreateDTO(); + if (recycle != null){ + BeanUtils.copyProperties(recycle, instance); + } + return instance; + } + + /** + * 添加任务详情 + * @param recycleDetailList 任务详情 + */ + public void setDetailList(List recycleDetailList) { + detailList = new ArrayList<>(); + for (RecycleDetail recycleDetail:recycleDetailList){ + recycleDetail.setUpdateUserId(this.getUpdateUserId()); + detailList.add(RecycleDetailCreateDTO.recycleDetailCreateDTO(recycleDetail)); + } + } + +} + diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleDetailCreateDTO.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleDetailCreateDTO.java new file mode 100644 index 0000000..fcb44d8 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleDetailCreateDTO.java @@ -0,0 +1,82 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; +import org.dubhe.recycle.domain.entity.RecycleDetail; +import org.springframework.beans.BeanUtils; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 创建垃圾回收任务详情DTO + * @date 2021-02-03 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class RecycleDetailCreateDTO extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private Long recycleId; + + @ApiModelProperty(value = "回收类型(0文件,1数据库表数据)", required = true) + @NotNull(message = "回收类型不能为空") + private Integer recycleType; + + @ApiModelProperty(value = "回收条件(回收表数据sql、回收文件绝对路径)", required = true) + @NotBlank(message = "回收条件不能为空") + private String recycleCondition; + + @ApiModelProperty(value = "回收说明") + private String recycleNote; + + @ApiModelProperty(value = "回收备注") + private String remark; + + @ApiModelProperty(value = "回收状态") + private Integer recycleStatus; + + + /** + * 创建 RecycleDetailCreateDTO + * @param recycleDetail 回收任务详情 + * @return RecycleDetailCreateDTO + */ + public static RecycleDetailCreateDTO recycleDetailCreateDTO(RecycleDetail recycleDetail){ + RecycleDetailCreateDTO instance = new RecycleDetailCreateDTO(); + if (recycleDetail != null){ + BeanUtils.copyProperties(recycleDetail, instance); + } + return instance; + } + +} + diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskDeleteDTO.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskDeleteDTO.java new file mode 100644 index 0000000..16db3d5 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskDeleteDTO.java @@ -0,0 +1,38 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.List; + +/** + * @description 回收任务执行列表 + * @date 2021-01-27 + */ +@Data +public class RecycleTaskDeleteDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "待回收任务ID") + @NotEmpty(message = "请选择回收任务") + private List recycleTaskIdList; + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskQueryDTO.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskQueryDTO.java new file mode 100644 index 0000000..ec74398 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/dto/RecycleTaskQueryDTO.java @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.base.PageQueryBase; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 回收任务列表查询条件 + * @date 2020-09-23 + */ +@Data +@Accessors(chain = true) +public class RecycleTaskQueryDTO extends PageQueryBase implements Serializable { + private static final long serialVersionUID = 2016225581479036412L; + + /** + * 具体值参考RecycleStatusEnum + */ + @ApiModelProperty(value = "回收任务状态(0:待回收,1:已回收,2:回收失败,3:回收中,4:还原中,5:已还原)") + private Integer recycleStatus; + + @ApiModelProperty(value = "回收类型(0文件,1数据库表数据)") + private Integer recycleType; + + @ApiModelProperty(value = "回收模块") + private String recycleModel; + + @ApiModelProperty(value = "回收任务ID") + private List recycleTaskIdList; + + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/Recycle.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/Recycle.java new file mode 100644 index 0000000..9c80153 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/Recycle.java @@ -0,0 +1,91 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; + +import java.sql.Timestamp; + +/** + * @description 垃圾回收主表 + * @date 2021-02-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +@TableName(value = "recycle") +public class Recycle extends BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 回收模块 + */ + @TableField(value = "recycle_module") + private Integer recycleModule; + + /** + * 回收延迟时间,以天为单位 + */ + @TableField(value = "recycle_delay_date") + private Timestamp recycleDelayDate; + + /** + * 回收定制化方式 + */ + @TableField(value = "recycle_custom") + private String recycleCustom; + + /** + * 回收状态 + */ + @TableField(value = "recycle_status") + private Integer recycleStatus; + + /** + * 回收说明 + */ + @TableField(value = "recycle_note") + private String recycleNote; + + /** + * 备注 + */ + @TableField(value = "remark") + private String remark; + + /** + * 回收响应信息 + */ + @TableField(value = "recycle_response") + private String recycleResponse; + + /** + * 还原定制化方式 + */ + @TableField(value = "restore_custom") + private String restoreCustom; + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/RecycleDetail.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/RecycleDetail.java new file mode 100644 index 0000000..e4fe2b6 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/domain/entity/RecycleDetail.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; + +/** + * @description 垃圾回收详情表 + * @date 2021-02-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +@TableName(value = "recycle_detail") +public class RecycleDetail extends BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField(value = "recycle_id") + private Long recycleId; + + /** + * 回收类型(0文件,1数据库表数据 + */ + @TableField(value = "recycle_type") + private Integer recycleType; + + /** + * 回收条件(回收表数据sql、回收文件绝对路径) + */ + @TableField(value = "recycle_condition") + private String recycleCondition; + + /** + * 回收状态 + */ + @TableField(value = "recycle_status") + private Integer recycleStatus; + + /** + * 回收说明 + */ + @TableField(value = "recycle_note") + private String recycleNote; + + /** + * 备注 + */ + @TableField(value = "remark") + private String remark; + + /** + * 回收响应信息 + */ + @TableField(value = "recycle_response") + private String recycleResponse; + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleModuleEnum.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleModuleEnum.java new file mode 100644 index 0000000..11cb250 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleModuleEnum.java @@ -0,0 +1,99 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.enums; + +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.exception.BusinessException; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.dubhe.biz.base.constant.ApplicationNameConst.*; +/** + * @description 垃圾回收模块枚举 + * @date 2020-09-17 + */ +public enum RecycleModuleEnum { + + + BIZ_TRAIN(1, "训练任务管理",SERVER_TRAIN), + BIZ_DATASET(2, "数据集管理",SERVER_DATA), + BIZ_NOTEBOOK(3, "Notebook",SERVER_NOTEBOOK), + BIZ_ALGORITHM(4, "算法管理",SERVER_ALGORITHM), + BIZ_IMAGE(5, "镜像管理",SERVER_IMAGE), + BIZ_MODEL_OPT(6, "模型优化",SERVER_OPTIMIZE), + BIZ_MODEL(7, "模型管理",SERVER_MODEL), + BIZ_DATAMEDICINE(8, "医学影像",SERVER_DATA_DCM), + BIZ_MEASURE(9, "度量管理",SERVER_MEASURE), + BIZ_SERVING(10, "云端Serving", SERVER_SERVING); + + private Integer value; + + private String desc; + + /** + * 模块服务名 + */ + private String server; + + RecycleModuleEnum(Integer value, String desc,String server) { + this.value = value; + this.desc = desc; + this.server = server; + } + + public Integer getValue() { + return this.value; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + /** + * 获取服务名 + * @param value 模块代号 + * @return 服务 + */ + public static String getServer(int value){ + for (RecycleModuleEnum recycleModuleEnum:RecycleModuleEnum.values()){ + if (value == recycleModuleEnum.getValue()){ + if (recycleModuleEnum.server == null){ + throw new BusinessException(recycleModuleEnum.desc + " 服务未配置服务名!"); + } + return recycleModuleEnum.server; + } + } + throw new BusinessException(value+" 服务不存在!"); + } + + /** + * 模块代号,模块名称 映射 + */ + public static final Map RECYCLE_MODULE_MAP; + static { + RECYCLE_MODULE_MAP = new LinkedHashMap<>(MagicNumConstant.SIXTEEN); + for (RecycleModuleEnum recycleModuleEnum:RecycleModuleEnum.values()){ + RECYCLE_MODULE_MAP.put(recycleModuleEnum.value,recycleModuleEnum.desc); + } + } + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleResourceEnum.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleResourceEnum.java new file mode 100644 index 0000000..5358662 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleResourceEnum.java @@ -0,0 +1,93 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.recycle.enums; + +import lombok.Getter; + +/** + * @description 资源回收枚举类 + * @date 2020-10-10 + */ +@Getter +public enum RecycleResourceEnum { + + /** + * 数据集文件回收 + */ + DATASET_RECYCLE_FILE("datasetRecycleFile", "数据集文件回收"), + + /** + * 医学数据集文件回收 + */ + DATAMEDICINE_RECYCLE_FILE("dataMedicineRecycleFile", "数据集文件回收"), + + /** + * 数据集版本文件回收 + */ + DATASET_RECYCLE_VERSION_FILE("datasetRecycleVersionFile", "数据集版本文件回收"), + + /** + * 云端Serving在线服务输入文件回收 + */ + SERVING_RECYCLE_FILE("servingRecycleFile", "云端Serving在线服务文件回收"), + /** + * 云端Serving批量服务输入文件回收 + */ + BATCH_SERVING_RECYCLE_FILE("batchServingRecycleFile", "云端Serving批量服务文件回收"), + + /** + * 标签组文件回收 + */ + LABEL_GROUP_RECYCLE_FILE("labelGroupRecycleFile", "标签组文件回收"), + + /** + * 度量文件回收 + */ + MEASURE_RECYCLE_FILE("measureRecycleFile", "度量文件回收"), + + /** + * 镜像回收 + */ + IMAGE_RECYCLE_FILE("imageRecycleFile", "镜像回收"), + + /** + * 算法文件回收 + */ + ALGORITHM_RECYCLE_FILE("algorithmRecycleFile", "算法文件回收"), + + /** + * 模型文件回收 + */ + MODEL_RECYCLE_FILE("modelRecycleFile", "模型文件回收"), + /** + * 模型优化文件回收 + */ + MODEL_OPT_RECYCLE_FILE("modelOptRecycleFile","模型优化文件回收"), + ; + + private String className; + + private String message; + + RecycleResourceEnum(String className, String message) { + this.className = className; + this.message = message; + } + + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleStatusEnum.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleStatusEnum.java new file mode 100644 index 0000000..9ffcadc --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleStatusEnum.java @@ -0,0 +1,57 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.enums; + +/** + * @description 垃圾回收状态 + * @date 2020-09-17 + */ +public enum RecycleStatusEnum { + + PENDING(0, "待删除"), + + SUCCEEDED(1, "已删除"), + FAILED(2, "删除失败"), + DOING(3,"删除中"), + + RESTORING(4,"还原中"), + RESTORED(5,"已还原"), + ; + + /** + * 编码 + */ + private Integer code; + + /** + * 描述 + */ + private String description; + + RecycleStatusEnum(int code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleTypeEnum.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleTypeEnum.java new file mode 100644 index 0000000..45d5760 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/enums/RecycleTypeEnum.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.enums; + +/** + * @description 垃圾回收类型 + * @date 2020-09-17 + */ +public enum RecycleTypeEnum { + + FILE(0, "文件"), + TABLE_DATA(1, "表数据"); + + private Integer code; + + private String type; + + + RecycleTypeEnum(Integer code, String type) { + this.code = code; + this.type = type; + } + + public Integer getCode() { + return code; + } + + public String getType() { + return type; + } +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/global/AbstractGlobalRecycle.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/global/AbstractGlobalRecycle.java new file mode 100644 index 0000000..db335d2 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/global/AbstractGlobalRecycle.java @@ -0,0 +1,215 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.global; + +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.dto.RecycleDetailCreateDTO; +import org.dubhe.recycle.domain.entity.Recycle; +import org.dubhe.recycle.domain.entity.RecycleDetail; +import org.dubhe.recycle.enums.RecycleStatusEnum; +import org.dubhe.recycle.service.CustomRecycleService; +import org.dubhe.recycle.service.RecycleService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +/** + * @description 数据集清理回收类 + * @date 2020-10-09 + */ +public abstract class AbstractGlobalRecycle implements CustomRecycleService { + + /** + * 回收服务 + */ + @Autowired + private RecycleService recycleService; + + /** + * 回收线程初始时间 + */ + private final ThreadLocal startTimeLocal = new ThreadLocal<>(); + + /** + * 重写自定义回收入口 + * 注意: + * 1:异步执行避免服务调用者等待 + * 2:不开启事务,避免大事务,且可以小事务分批执行 + * @param dto 资源回收创建对象 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void recycle(RecycleCreateDTO dto) { + if (dto == null || dto.getId() == null){ + // 非法入参 + return; + } + long userId = dto.getUpdateUserId(); + try { + // 回收前置处理 + // 设置 线程回收开始时间 + startTimeLocal.set(System.currentTimeMillis()); + Recycle recycle = getRecycle(dto); + // 标记执行中 + recycleService.updateRecycle(recycle, RecycleStatusEnum.DOING,null,userId); + try { + // 回收业务详情 + clear(dto); + // 回收成功 + recycleService.updateRecycle(recycle, RecycleStatusEnum.SUCCEEDED,null,userId); + } catch (Exception e) { + // 回收失败 + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "custom recycle {} error {},", recycle.getId(), e); + recycleService.updateRecycle(recycle, RecycleStatusEnum.FAILED,e.getMessage(),userId); + } + }finally { + // 清除 线程回收开始时间 + startTimeLocal.remove(); + } + } + + /** + * 清理回收详情 + * @param dto 资源回收创建对象 + */ + private void clear(RecycleCreateDTO dto){ + long userId = dto.getUpdateUserId(); + for (RecycleDetailCreateDTO detail:dto.getDetailList()) { + RecycleDetail recycleDetail = getRecycleDetail(detail); + if (RecycleStatusEnum.SUCCEEDED.getCode().equals(recycleDetail.getRecycleStatus())){ + // 跳过已删除成功任务详情 + continue; + } + recycleService.updateRecycleDetail(recycleDetail, RecycleStatusEnum.DOING,null,userId); + try { + boolean continued = clearDetail(detail, dto); + recycleService.updateRecycleDetail(recycleDetail, RecycleStatusEnum.SUCCEEDED, null, userId); + if (!continued){ + // 中断任务详情回收 + break; + } + }catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "custom recycle detail{} error {},", recycleDetail.getId(), e); + recycleService.updateRecycleDetail(recycleDetail, RecycleStatusEnum.FAILED,e.getMessage(),userId); + } + // 同步最新状态,以备this.addNewRecycleTask + detail.setRecycleStatus(recycleDetail.getRecycleStatus()); + } + } + + /** + * 单个回收详情清理 + * + * @param detail 数据清理详情参数 + * @param dto 资源回收创建对象 + * @return true 继续执行,false 中断任务详情回收(本次无法执行完毕,创建新任务到下次执行) + */ + protected abstract boolean clearDetail(RecycleDetailCreateDTO detail,RecycleCreateDTO dto) throws Exception; + + /** + * 获取Recycle + * @param recycleCreateDTO 数据清理参数 + * @return Recycle + */ + private Recycle getRecycle(RecycleCreateDTO recycleCreateDTO){ + Recycle recycle = new Recycle(); + BeanUtils.copyProperties(recycleCreateDTO, recycle); + return recycle; + } + + /** + * 获取RecycleDetail + * @param recycleDetailCreateDTO 数据清理详情参数 + * @return RecycleDetail + */ + private RecycleDetail getRecycleDetail(RecycleDetailCreateDTO recycleDetailCreateDTO){ + RecycleDetail recycleDetail = new RecycleDetail(); + BeanUtils.copyProperties(recycleDetailCreateDTO, recycleDetail); + return recycleDetail; + } + + + /** + * 超时校验 + * + * @return true: 未超时,可继续资源回收 false: 已超时 + */ + protected boolean validateOverTime() { + return (System.currentTimeMillis() - startTimeLocal.get()) / NumberConstant.NUMBER_1000 < getRecycleOverSecond(); + } + + + /** + * 初始化开始时间 + * + */ + protected void initOverTime() { + startTimeLocal.set(System.currentTimeMillis()); + } + + /** + * 新增回收任务 + * + * @param dto 数据清理参数 + */ + protected void addNewRecycleTask(RecycleCreateDTO dto) { + if (Objects.nonNull(dto)) { + recycleService.createRecycleTask(dto); + } + } + + /** + * 重写自定义还原入口 + * + * @param dto + */ + @Override + public void restore(RecycleCreateDTO dto){ + if (dto == null || dto.getId() == null){ + // 非法入参 + return; + } + long userId = dto.getUpdateUserId(); + Recycle recycle = getRecycle(dto); + // 标记还原中 + recycleService.updateRecycle(recycle, RecycleStatusEnum.RESTORING,null,userId); + try { + // 业务回收 + rollback(dto); + // 还原成功 + recycleService.updateRecycle(recycle, RecycleStatusEnum.RESTORED,null,userId); + } catch (Exception e) { + // 还原失败,回归初始状态 + recycleService.updateRecycle(recycle, RecycleStatusEnum.PENDING, e.getMessage(),userId); + // 抛出异常 + throw e; + } + } + + /** + * 业务数据还原 + * + * @param dto 数据还原参数 + */ + protected abstract void rollback(RecycleCreateDTO dto); + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/interceptor/RecycleCallInterceptor.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/interceptor/RecycleCallInterceptor.java new file mode 100644 index 0000000..b4c5790 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/interceptor/RecycleCallInterceptor.java @@ -0,0 +1,57 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.recycle.interceptor; + +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.recycle.utils.RecycleTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @description 资源回收调用拦截器 + * @date 2021-01-21 + */ +@Component +public class RecycleCallInterceptor extends HandlerInterceptorAdapter { + + @Autowired + private RecycleTool recycleTool; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String uri = request.getRequestURI(); + LogUtil.debug(LogEnum.GARBAGE_RECYCLE,"资源回收接收到请求,URI:{}",uri); + String token = request.getHeader(RecycleTool.RECYCLE_TOKEN); + if (StringUtils.isBlank(token)){ + LogUtil.warn(LogEnum.GARBAGE_RECYCLE,"资源回收没有token配置【{}】,URI:{}",RecycleTool.RECYCLE_TOKEN,uri); + return false; + } + boolean pass = recycleTool.validateToken(token); + if (!pass){ + LogUtil.warn(LogEnum.GARBAGE_RECYCLE,"资源回收token:【{}】 验证不通过,URI:{}",token,uri); + } + return pass; + } + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/rest/RecycleCallController.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/rest/RecycleCallController.java new file mode 100644 index 0000000..55ccfef --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/rest/RecycleCallController.java @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.recycle.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.service.CustomRecycleService; +import org.dubhe.recycle.utils.RecycleTool; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @description 资源回收远程调用处理类 + * @date 2021-01-21 + */ +@Api(tags = "通用:资源回收远程调用") +@RestController +@RequestMapping(RecycleTool.RECYCLE_CALL_PATH) +public class RecycleCallController { + + + @PostMapping(value = RecycleTool.BIZ_RECYCLE) + @ApiOperation("资源回收远程调用统一入口") + public DataResponseBody recycle(@ApiParam(type = "head") @RequestHeader(name= RecycleTool.RECYCLE_TOKEN) String token + , @Validated @RequestBody RecycleCreateDTO dto) { + CustomRecycleService customRecycleService = SpringContextHolder.getBean(dto.getRecycleCustom()); + if (customRecycleService == null){ + return DataResponseFactory.failed("本服务未实现自定义资源删除!"); + }else { + // 因作业量大,异步执行 + customRecycleService.recycle(dto); + return DataResponseFactory.successWithMsg("资源删除正在异步处理中。"); + } + } + + @PostMapping(value = RecycleTool.BIZ_RESTORE) + @ApiOperation("资源还原远程调用统一入口") + public DataResponseBody restore(@ApiParam(type = "head") @RequestHeader(name= RecycleTool.RECYCLE_TOKEN) String token + , @Validated @RequestBody RecycleCreateDTO dto) { + CustomRecycleService customRecycleService = SpringContextHolder.getBean(dto.getRestoreCustom()); + if (customRecycleService == null){ + return DataResponseFactory.failed("本服务未实现自定义资源还原!"); + }else { + // 同步执行 + customRecycleService.restore(dto); + return DataResponseFactory.successWithMsg("还原成功!"); + } + } + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/CustomRecycleService.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/CustomRecycleService.java new file mode 100644 index 0000000..97d8d1a --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/CustomRecycleService.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.service; + +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.springframework.scheduling.annotation.Async; + +/** + * @description 垃圾回收执行接口 + * @date 2021-01-20 + */ +public interface CustomRecycleService { + + /** + * 自定义回收入口 + * 注意: + * 1:异步执行避免服务调用者等待 + * 2:不开启事务,避免大事务,且可以小事务分批执行 + * @param dto 资源回收创建对象 + */ + @Async + void recycle(RecycleCreateDTO dto); + + /** + * 自定义回收超时时间(单位:秒) + * @return 默认60秒 + */ + default long getRecycleOverSecond(){ + return 60L; + } + + /** + * 还原资源回收 + * @param dto 资源回收创建对象 + */ + default void restore(RecycleCreateDTO dto){ + throw new BusinessException("还原资源回收暂未实现!"); + } + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/RecycleService.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/RecycleService.java new file mode 100644 index 0000000..9995b69 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/RecycleService.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.service; + +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.entity.Recycle; +import org.dubhe.recycle.domain.entity.RecycleDetail; +import org.dubhe.recycle.enums.RecycleStatusEnum; + +/** + * @description 通用回收垃圾 服务类 + * @date 2021-02-03 + */ +public interface RecycleService { + + + /** + * 创建垃圾回收任务 + * + * @param recycleCreateDTO 垃圾回收任务信息 + */ + void createRecycleTask(RecycleCreateDTO recycleCreateDTO); + + /** + * 修改回收任务状态 + * + * @param recycle 回收任务 + * @param statusEnum 回收状态 + * @param recycleResponse 回收响应 + * @param userId 操作用户 + */ + void updateRecycle(Recycle recycle, RecycleStatusEnum statusEnum, String recycleResponse, long userId); + + /** + * 修改回收任务详情状态 + * + * @param recycleDetail 回收任务详情 + * @param statusEnum 回收状态 + * @param recycleResponse 回收响应 + * @param userId 操作用户 + */ + void updateRecycleDetail(RecycleDetail recycleDetail, RecycleStatusEnum statusEnum, String recycleResponse, long userId); + +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/impl/RecycleServiceImpl.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/impl/RecycleServiceImpl.java new file mode 100644 index 0000000..2eb7801 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/service/impl/RecycleServiceImpl.java @@ -0,0 +1,165 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.recycle.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.DateUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.config.NfsConfig; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.recycle.config.RecycleConfig; +import org.dubhe.recycle.dao.RecycleDetailMapper; +import org.dubhe.recycle.dao.RecycleMapper; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.dto.RecycleDetailCreateDTO; +import org.dubhe.recycle.domain.entity.Recycle; +import org.dubhe.recycle.domain.entity.RecycleDetail; +import org.dubhe.recycle.enums.RecycleStatusEnum; +import org.dubhe.recycle.enums.RecycleTypeEnum; +import org.dubhe.recycle.service.RecycleService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.sql.Timestamp; +import java.util.Objects; + +/** + * @description 通用垃圾回收 实现类 + * @date 2021-02-03 + */ +@Service +public class RecycleServiceImpl implements RecycleService { + + @Autowired + private RecycleMapper recycleMapper; + + @Autowired + private RecycleDetailMapper recycleDetailMapper; + + @Autowired + private RecycleConfig recycleConfig; + + @Autowired + private NfsConfig nfsConfig; + + @Autowired + private UserContextService userContextService; + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + /** + * 创建垃圾回收任务 + * + * @param recycleCreateDTO 垃圾回收任务信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void createRecycleTask(RecycleCreateDTO recycleCreateDTO) { + if (CollectionUtil.isEmpty(recycleCreateDTO.getDetailList())) { + throw new BusinessException("请添加回收详情!"); + } + //获取当前用户信息 + UserContext curUser = userContextService.getCurUser(); + //设置默认回收延迟时间 + if (recycleCreateDTO.getRecycleDelayDate() <= 0) { + recycleCreateDTO.setRecycleDelayDate(recycleConfig.getDate()); + } + for (RecycleDetailCreateDTO detail : recycleCreateDTO.getDetailList()) { + //如果是删除文件任务,校验根目录及系统环境 + if (Objects.equals(detail.getRecycleType(), RecycleTypeEnum.FILE.getCode()) && + fileStoreApi.formatPath(detail.getRecycleCondition()).startsWith(nfsConfig.getRootDir() + nfsConfig.getBucket())) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "User {} created recycle task failed,file sourcePath :{} invalid", curUser.getUsername(), detail.getRecycleCondition()); + throw new BusinessException("创建回收文件任务失败"); + } + } + long createUserId = Objects.isNull(recycleCreateDTO.getCreateUserId()) ? curUser.getId() : recycleCreateDTO.getCreateUserId(); + long updateUserId = Objects.isNull(recycleCreateDTO.getUpdateUserId()) ? curUser.getId() : recycleCreateDTO.getUpdateUserId(); + // 组装回收任务 + Recycle recycle = new Recycle(); + BeanUtils.copyProperties(recycleCreateDTO, recycle); + recycle.setRecycleStatus(RecycleStatusEnum.PENDING.getCode()); + recycle.setCreateUserId(createUserId); + recycle.setUpdateUserId(updateUserId); + recycle.setRecycleDelayDate(DateUtil.getRecycleTime(recycleCreateDTO.getRecycleDelayDate())); + recycleMapper.insert(recycle); + // 组装任务详情 + for (RecycleDetailCreateDTO detail : recycleCreateDTO.getDetailList()) { + RecycleDetail recycleDetail = new RecycleDetail(); + BeanUtils.copyProperties(detail, recycleDetail); + recycleDetail.setRecycleId(recycle.getId()); + if (recycleDetail.getRecycleStatus() == null) { + recycleDetail.setRecycleStatus(RecycleStatusEnum.PENDING.getCode()); + } + recycleDetail.setCreateUserId(createUserId); + recycleDetail.setUpdateUserId(updateUserId); + recycleDetailMapper.insert(recycleDetail); + } + } + + /** + * 修改回收任务状态 + * + * @param recycle 回收任务 + * @param statusEnum 回收状态 + * @param recycleResponse 回收响应 + * @param userId 操作用户 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateRecycle(Recycle recycle, RecycleStatusEnum statusEnum, String recycleResponse, long userId) { + recycle.setRecycleStatus(statusEnum.getCode()); + // 确保响应信息会刷新 + if (recycleResponse == null) { + recycleResponse = statusEnum.getDescription(); + } + recycle.setRecycleResponse(StringUtils.truncationString(recycleResponse, MagicNumConstant.FIVE_HUNDRED)); + recycle.setUpdateUserId(userId); + recycle.setRecycleDelayDate(new Timestamp(System.currentTimeMillis())); + recycleMapper.updateById(recycle); + } + + /** + * 修改回收任务详情状态 + * + * @param recycleDetail 回收任务详情 + * @param statusEnum 回收状态 + * @param recycleResponse 回收响应 + * @param userId 操作用户 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateRecycleDetail(RecycleDetail recycleDetail, RecycleStatusEnum statusEnum, String recycleResponse, long userId) { + recycleDetail.setRecycleStatus(statusEnum.getCode()); + // 确保响应信息会刷新 + if (recycleResponse == null) { + recycleResponse = statusEnum.getDescription(); + } + recycleDetail.setRecycleResponse(StringUtils.truncationString(recycleResponse, MagicNumConstant.FIVE_HUNDRED)); + recycleDetail.setUpdateUserId(userId); + recycleDetailMapper.updateById(recycleDetail); + } +} diff --git a/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/utils/RecycleTool.java b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/utils/RecycleTool.java new file mode 100644 index 0000000..466ff38 --- /dev/null +++ b/dubhe-server/common-recycle/src/main/java/org/dubhe/recycle/utils/RecycleTool.java @@ -0,0 +1,241 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.recycle.utils; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.constant.StringConstant; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.utils.AesUtil; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.api.impl.ShellFileStoreApiImpl; +import org.dubhe.biz.file.utils.IOUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.recycle.enums.RecycleModuleEnum; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.util.Date; + +/** + * @description 资源回收工具类 + * @date 2021-01-21 + */ +@Component +public class RecycleTool { + + /** + * token秘钥 + */ + @Value("${recycle.call.token.secret-key}") + private String secretKey; + /** + * token超时时间 + */ + @Value("${recycle.call.token.expire-seconds}") + private Integer expireSeconds; + + /** + * 资源无效文件临时存放目录(默认/tmp/empty_) + */ + @Value("${recycle.file-tmp-path.recycle:/tmp/empty_}") + private String recycleFileTmpPath; + + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + @Value("${storage.file-store}") + private String ip; + + @Value("${data.server.userName}") + private String userName; + + /** + * 资源回收授权token + */ + public static final String RECYCLE_TOKEN = AuthConst.COMMON_TOKEN; + /** + * 路径 + */ + public static final String RECYCLE_CALL_PATH = StringConstant.RECYCLE_CALL_URI; + /** + * 回收业务 + */ + public static final String BIZ_RECYCLE = "recycle"; + /** + * 还原业务 + */ + public static final String BIZ_RESTORE = "restore"; + /** + * 权限匹配地址 + */ + public static final String MATCH_RECYCLE_PATH = RECYCLE_CALL_PATH + "**"; + + + /** + * 生成token + * + * @return token + */ + public String generateToken() { + String expireTime = DateUtil.format( + DateUtil.offset(new Date(), DateField.SECOND, expireSeconds), + DatePattern.PURE_DATETIME_PATTERN + ); + return AesUtil.encrypt(expireTime, secretKey); + } + + /** + * 验证token + * + * @param token 待校验token + * @return true token有效 false 无效 + */ + public boolean validateToken(String token) { + String expireTime = AesUtil.decrypt(token, secretKey); + if (StringUtils.isEmpty(expireTime)) { + return false; + } + String nowTime = DateUtil.format( + new Date(), + DatePattern.PURE_DATETIME_PATTERN + ); + return expireTime.compareTo(nowTime) > 0; + } + + + /** + * 获取调用地址 + * + * @param model 模块代号 RecycleModuleEnum.value + * @param biz 模块业务 + * @return String 回调地址 + */ + public static String getCallUrl(int model, String biz) { + return "http://" + RecycleModuleEnum.getServer(model) + RECYCLE_CALL_PATH + biz; + } + + /** + * 生成回收说明 + * @param baseNote 基本说明信息 + * @param bizId 业务ID + * @return string回收说明 + */ + public static String generateRecycleNote(String baseNote, long bizId) { + return String.format("%s ID:%d", baseNote, bizId); + } + + /** + * 生成回收说明 + * @param baseNote 基本说明信息 + * @param bizId 业务ID + * @return string回收说明 + */ + public static String generateRecycleNote(String baseNote, String name, long bizId) { + return String.format("%s name:%s ID:%d", baseNote, name, bizId); + } + + /** + * 实时删除临时目录完整路径无效文件 + * + * @param sourcePath 删除路径 + */ + public void delTempInvalidResources(String sourcePath) { + String resMsg = deleteFileByCMD(sourcePath, RandomUtil.randomString(MagicNumConstant.TWO)); + if (StrUtil.isNotEmpty(resMsg)) { + throw new BusinessException(ResponseCode.ERROR, resMsg); + } + } + + + /** + * 回收天枢一站式平台中的无效文件资源 + * 处理方式:获取到回收任务表中的无效文件路径,通过linux命令进行具体删除 + * 文件路径必须满足格式如:/nfs/当前系统环境/具体删除的文件或文件夹(至少三层目录) + * + * @param recycleConditionPath 文件回收绝对路径 + * @param randomPath emptyDir目录补偿位置 + * @return String 回收任务失败返回的失败信息 + */ + private String deleteFileByCMD(String recycleConditionPath, String randomPath) { + LogUtil.info(LogEnum.GARBAGE_RECYCLE, "RecycleTool deleteFileByCMD recycleConditionPath:{}, randomPath:{}", recycleConditionPath, randomPath); + + String sourcePath = fileStoreApi.formatPath(recycleConditionPath + File.separator); + //判断该路径是否存在文件或文件夹 + String nfsBucket = fileStoreApi.formatPath(fileStoreApi.getRootDir() + fileStoreApi.getBucket() + File.separator); + sourcePath = sourcePath.startsWith(nfsBucket) ? sourcePath : fileStoreApi.formatPath(nfsBucket + sourcePath); + Process process = null; + try { + if (sourcePath.length() > nfsBucket.length()) { + String emptyDir = recycleFileTmpPath + randomPath + StrUtil.SLASH; + LogUtil.info(LogEnum.GARBAGE_RECYCLE, "recycle task sourcePath:{},emptyDir:{}", sourcePath, emptyDir); + process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", String.format(ShellFileStoreApiImpl.DEL_COMMAND, userName, ip, emptyDir, emptyDir, sourcePath, emptyDir, sourcePath)}); + } + return processRecycle(process); + + } catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "文件资源回收失败! Exception:{}", e); + return "文件资源回收失败! sourcePath:" + sourcePath + " Exception:" + e.getMessage(); + } + + } + + /** + * 执行服务器命令 + * + * @param process Process对象 + * @return null 成功执行,其他:异常结束信息 + */ + public String processRecycle(Process process) { + InputStreamReader stream = new InputStreamReader(process.getErrorStream()); + BufferedReader reader = new BufferedReader(stream); + StringBuilder errMessage = new StringBuilder(); + try { + while (reader.read() != MagicNumConstant.NEGATIVE_ONE) { + errMessage.append(reader.readLine()); + } + int status = process.waitFor(); + if (status == 0) { + // 成功 + return null; + } else { + // 失败 + LogUtil.info(LogEnum.GARBAGE_RECYCLE, "recycleSourceIsOk is failure,errorMsg:{},processStatus:{}", errMessage.toString(), status); + return errMessage.length() > 0 ? errMessage.toString() : "文件删除失败!"; + } + } catch (Exception e) { + LogUtil.error(LogEnum.GARBAGE_RECYCLE, "recycleSourceIsOk is failure: {} ", e); + return e.getMessage(); + } finally { + IOUtil.close(reader, stream); + } + } +} diff --git a/dubhe-server/deploy-base.sh b/dubhe-server/deploy-base.sh new file mode 100644 index 0000000..fff94ba --- /dev/null +++ b/dubhe-server/deploy-base.sh @@ -0,0 +1,44 @@ +#!/bin/bash +#基础部署脚本 +#环境,用以区分部署的命名空间,日志路径 +ENV=$1 +#本文件绝对路径 +SOURCE_CODE_PATH=$(cd $(dirname ${BASH_SOURCE[0]}); pwd ) + +#harbor 地址 +HARBOR_URL=harbor.dubhe.ai +#harbor 用户名 +HARBOR_USERNAME=admin +#harbor 密码 +HARBOR_PWD=Harbor12345 +#文件存储服务 共享目录 +FS_PATH=/nfs +#容器日志路径 +CONTAINER_LOG_PATH=/logs +#宿主机日志路径 +HOST_LOG_PATH=/logs/dubhe-${ENV} + +#删除镜像 +delete_old_image() { + docker rmi -f ${HARBOR_URL}/dubhe/dubhe-spring-cloud-k8s:${ENV} +} +#构建镜像 +build_image() { + cd ${SOURCE_CODE_PATH} && docker build -t ${HARBOR_URL}/dubhe/dubhe-spring-cloud-k8s:${ENV} . +} +#推送镜像到harbor +push_image() { + docker login -u ${HARBOR_USERNAME} -p ${HARBOR_PWD} ${HARBOR_URL} + docker push ${HARBOR_URL}/dubhe/dubhe-spring-cloud-k8s:${ENV} +} +#编译打包源码 +mvn_build() { + # -T 1C 每核心打包一个工程 + # -Dmaven.test.skip=true 跳过测试代码的编译 + # -Dmaven.compile.fork=true 多线程编译 + cd ${SOURCE_CODE_PATH} && mvn clean compile package -T 1C -Dmaven.test.skip=true -Dmaven.compile.fork=true +} + +update_k8s_yaml() { + sed -i "s#harbor.test.com#${HARBOR_URL}#g;s#fsPath#${FS_PATH}#g;s#env-value#${ENV}#g;s#containerLogPath#${CONTAINER_LOG_PATH}#g;s#hostLogPath#${HOST_LOG_PATH}#g;s#gatewayNodePort#${GATEWAY_NODE_PORT}#g" ${SOURCE_CODE_PATH}/deploy/*/* +} \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/deploy-individual.sh b/dubhe-server/deploy/cloud/deploy-individual.sh new file mode 100644 index 0000000..9ecd1f1 --- /dev/null +++ b/dubhe-server/deploy/cloud/deploy-individual.sh @@ -0,0 +1,41 @@ +#!/bin/bash +#引用基础脚本 +source $(cd $(dirname ${BASH_SOURCE[0]}); pwd )/../../deploy-base.sh + +#网关暴露端口 +GATEWAY_NODE_PORT=$2 +#模块列表 +MODULES=${@:3} + +#删除服务 +delete_k8s_app() { + echo "start delete ${MODULES}" + for i in ${MODULES} + do + echo "kubectl delete -f "server-${i}.yaml" -n dubhe-${ENV}" + cd ${SOURCE_CODE_PATH}/deploy/cloud && kubectl delete -f "server-${i}.yaml" -n dubhe-${ENV} + done +} +#配置gateway端口 +update_gateway_node_port() { + sed -i "s#gatewayNodePort#${GATEWAY_NODE_PORT}#g" ${SOURCE_CODE_PATH}/deploy/*/* +} +#部署服务 +deploy_k8s_app() { + echo "start deploy ${MODULES}" + kubectl create ns dubhe-${ENV} + for i in ${MODULES} + do + echo "kubectl apply -f "server-${i}.yaml" -n dubhe-${ENV}" + cd ${SOURCE_CODE_PATH}/deploy/cloud && kubectl apply -f "server-${i}.yaml" -n dubhe-${ENV} + done +} + +delete_k8s_app +delete_old_image +update_k8s_yaml +update_gateway_node_port +mvn_build +build_image +push_image +deploy_k8s_app diff --git a/dubhe-server/deploy/cloud/deploy.sh b/dubhe-server/deploy/cloud/deploy.sh new file mode 100644 index 0000000..81fea02 --- /dev/null +++ b/dubhe-server/deploy/cloud/deploy.sh @@ -0,0 +1,28 @@ +#!/bin/bash +#引用基础脚本 +source $(cd $(dirname ${BASH_SOURCE[0]}); pwd )/../../deploy-base.sh + +#网关暴露端口 +GATEWAY_NODE_PORT=$2 + +#删除服务 +delete_k8s_app() { + kubectl delete ns dubhe-${ENV} +} +#配置gateway端口 +update_gateway_node_port() { + sed -i "s#gatewayNodePort#${GATEWAY_NODE_PORT}#g" ${SOURCE_CODE_PATH}/deploy/*/* +} +#部署服务 +deploy_k8s_app() { + cd ${SOURCE_CODE_PATH}/deploy/cloud && kubectl create ns dubhe-${ENV} && kubectl apply -f server-dubhe.yaml -n dubhe-${ENV} +} + +delete_k8s_app +delete_old_image +update_k8s_yaml +update_gateway_node_port +mvn_build +build_image +push_image +deploy_k8s_app diff --git a/dubhe-server/deploy/cloud/server-admin.yaml b/dubhe-server/deploy/cloud/server-admin.yaml new file mode 100644 index 0000000..ce239fc --- /dev/null +++ b/dubhe-server/deploy/cloud/server-admin.yaml @@ -0,0 +1,78 @@ +################################################################################################## +# admin +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: admin + labels: + app: admin + service: admin +spec: + type: NodePort + ports: + - port: 8870 + name: http + selector: + app: admin +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: admin + labels: + account: admin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: admin-v1 + labels: + app: admin + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: admin + version: v1 + template: + metadata: + labels: + app: admin + version: v1 + spec: + serviceAccountName: admin + containers: + - name: admin + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "admin-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/admin-dump.hprof" + ports: + - containerPort: 8870 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-auth.yaml b/dubhe-server/deploy/cloud/server-auth.yaml new file mode 100644 index 0000000..88969b2 --- /dev/null +++ b/dubhe-server/deploy/cloud/server-auth.yaml @@ -0,0 +1,78 @@ +################################################################################################## +# auth +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: auth + labels: + app: auth + service: auth +spec: + type: NodePort + ports: + - port: 8866 + name: http + selector: + app: auth +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: auth + labels: + account: auth +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth-v1 + labels: + app: auth + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: auth + version: v1 + template: + metadata: + labels: + app: auth + version: v1 + spec: + serviceAccountName: auth + containers: + - name: auth + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "auth-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/auth-dump.hprof" + ports: + - containerPort: 8866 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-algorithm.yaml b/dubhe-server/deploy/cloud/server-dubhe-algorithm.yaml new file mode 100644 index 0000000..fbd7734 --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-algorithm.yaml @@ -0,0 +1,78 @@ +################################################################################################## +# dubhe-algorithm +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-algorithm + labels: + app: dubhe-algorithm + service: dubhe-algorithm +spec: + type: NodePort + ports: + - port: 8889 + name: http + selector: + app: dubhe-algorithm +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-algorithm + labels: + account: dubhe-algorithm +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-algorithm-v1 + labels: + app: dubhe-algorithm + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-algorithm + version: v1 + template: + metadata: + labels: + app: dubhe-algorithm + version: v1 + spec: + serviceAccountName: dubhe-algorithm + containers: + - name: dubhe-algorithm + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-algorithm-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-algorithm-dump.hprof" + ports: + - containerPort: 8889 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-data-dcm.yaml b/dubhe-server/deploy/cloud/server-dubhe-data-dcm.yaml new file mode 100644 index 0000000..5da39ac --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-data-dcm.yaml @@ -0,0 +1,78 @@ +################################################################################################## +# dubhe-data-dcm +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-data-dcm + labels: + app: dubhe-data-dcm + service: dubhe-data-dcm +spec: + type: NodePort + ports: + - port: 8011 + name: http + selector: + app: dubhe-data-dcm +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-data-dcm + labels: + account: dubhe-data-dcm +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-data-dcm-v1 + labels: + app: dubhe-data-dcm + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-data-dcm + version: v1 + template: + metadata: + labels: + app: dubhe-data-dcm + version: v1 + spec: + serviceAccountName: dubhe-data-dcm + containers: + - name: dubhe-data-dcm + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-data-dcm-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-data-dcm-dump.hprof" + ports: + - containerPort: 8011 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-data-task.yaml b/dubhe-server/deploy/cloud/server-dubhe-data-task.yaml new file mode 100644 index 0000000..c04a1db --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-data-task.yaml @@ -0,0 +1,81 @@ +################################################################################################## +# dubhe-data-task +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-data-task + labels: + app: dubhe-data-task + service: dubhe-data-task +spec: + type: NodePort + ports: + - port: 8801 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-data-task +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-data-task + labels: + account: dubhe-data-task +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-data-task-v1 + labels: + app: dubhe-data-task + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-data-task + version: v1 + template: + metadata: + labels: + app: dubhe-data-task + version: v1 + spec: + serviceAccountName: dubhe-data-task + containers: + - name: dubhe-data-task + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-data-task-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-data-task-dump.hprof" + ports: + - containerPort: 8801 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-data.yaml b/dubhe-server/deploy/cloud/server-dubhe-data.yaml new file mode 100644 index 0000000..6efb88e --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-data.yaml @@ -0,0 +1,81 @@ +################################################################################################## +# dubhe-data +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-data + labels: + app: dubhe-data + service: dubhe-data +spec: + type: NodePort + ports: + - port: 8823 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-data +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-data + labels: + account: dubhe-data +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-data-v1 + labels: + app: dubhe-data + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-data + version: v1 + template: + metadata: + labels: + app: dubhe-data + version: v1 + spec: + serviceAccountName: dubhe-data + containers: + - name: dubhe-data + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-data-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms4096m -Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-data-dump.hprof" + ports: + - containerPort: 8823 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-image.yaml b/dubhe-server/deploy/cloud/server-dubhe-image.yaml new file mode 100644 index 0000000..3a62abb --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-image.yaml @@ -0,0 +1,78 @@ +################################################################################################## +# dubhe-image +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-image + labels: + app: dubhe-image + service: dubhe-image +spec: + type: NodePort + ports: + - port: 8822 + name: http + selector: + app: dubhe-image +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-image + labels: + account: dubhe-image +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-image-v1 + labels: + app: dubhe-image + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-image + version: v1 + template: + metadata: + labels: + app: dubhe-image + version: v1 + spec: + serviceAccountName: dubhe-image + containers: + - name: dubhe-image + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-image-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-image-dump.hprof" + ports: + - containerPort: 8822 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-k8s.yaml b/dubhe-server/deploy/cloud/server-dubhe-k8s.yaml new file mode 100644 index 0000000..d330313 --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-k8s.yaml @@ -0,0 +1,81 @@ +################################################################################################## +# dubhe-k8s +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-k8s + labels: + app: dubhe-k8s + service: dubhe-k8s +spec: + type: NodePort + ports: + - port: 8960 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-k8s +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-k8s + labels: + account: dubhe-k8s +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-k8s-v1 + labels: + app: dubhe-k8s + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-k8s + version: v1 + template: + metadata: + labels: + app: dubhe-k8s + version: v1 + spec: + serviceAccountName: dubhe-k8s + containers: + - name: dubhe-k8s + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-k8s-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-k8s-dump.hprof" + ports: + - containerPort: 8960 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-measure.yaml b/dubhe-server/deploy/cloud/server-dubhe-measure.yaml new file mode 100644 index 0000000..a06952d --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-measure.yaml @@ -0,0 +1,78 @@ +################################################################################################## +# dubhe-measure +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-measure + labels: + app: dubhe-measure + service: dubhe-measure +spec: + type: NodePort + ports: + - port: 8821 + name: http + selector: + app: dubhe-measure +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-measure + labels: + account: dubhe-measure +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-measure-v1 + labels: + app: dubhe-measure + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-measure + version: v1 + template: + metadata: + labels: + app: dubhe-measure + version: v1 + spec: + serviceAccountName: dubhe-measure + containers: + - name: dubhe-measure + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-measure-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-measure-dump.hprof" + ports: + - containerPort: 8821 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-model.yaml b/dubhe-server/deploy/cloud/server-dubhe-model.yaml new file mode 100644 index 0000000..25a308b --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-model.yaml @@ -0,0 +1,81 @@ +################################################################################################## +# dubhe-model +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-model + labels: + app: dubhe-model + service: dubhe-model +spec: + type: NodePort + ports: + - port: 8888 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-model +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-model + labels: + account: dubhe-model +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-model-v1 + labels: + app: dubhe-model + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-model + version: v1 + template: + metadata: + labels: + app: dubhe-model + version: v1 + spec: + serviceAccountName: dubhe-model + containers: + - name: dubhe-model + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-model-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-model-dump.hprof" + ports: + - containerPort: 8888 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-notebook.yaml b/dubhe-server/deploy/cloud/server-dubhe-notebook.yaml new file mode 100644 index 0000000..0f62f9b --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-notebook.yaml @@ -0,0 +1,81 @@ +################################################################################################## +# dubhe-notebook +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-notebook + labels: + app: dubhe-notebook + service: dubhe-notebook +spec: + type: NodePort + ports: + - port: 8801 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-notebook +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-notebook + labels: + account: dubhe-notebook +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-notebook-v1 + labels: + app: dubhe-notebook + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-notebook + version: v1 + template: + metadata: + labels: + app: dubhe-notebook + version: v1 + spec: + serviceAccountName: dubhe-notebook + containers: + - name: dubhe-notebook + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-notebook-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-notebook-dump.hprof" + ports: + - containerPort: 8801 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-optimize.yaml b/dubhe-server/deploy/cloud/server-dubhe-optimize.yaml new file mode 100644 index 0000000..574e1cb --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-optimize.yaml @@ -0,0 +1,81 @@ +################################################################################################## +# dubhe-optimize +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-optimize + labels: + app: dubhe-optimize + service: dubhe-optimize +spec: + type: NodePort + ports: + - port: 8899 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-optimize +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-optimize + labels: + account: dubhe-optimize +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-optimize-v1 + labels: + app: dubhe-optimize + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-optimize + version: v1 + template: + metadata: + labels: + app: dubhe-optimize + version: v1 + spec: + serviceAccountName: dubhe-optimize + containers: + - name: dubhe-optimize + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-optimize-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-optimize-dump.hprof" + ports: + - containerPort: 8899 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-serving-gateway.yaml b/dubhe-server/deploy/cloud/server-dubhe-serving-gateway.yaml new file mode 100644 index 0000000..f48bba6 --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-serving-gateway.yaml @@ -0,0 +1,82 @@ +################################################################################################## +# dubhe-serving-gateway +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-serving-gateway + labels: + app: dubhe-serving-gateway + service: dubhe-serving-gateway +spec: + type: NodePort + ports: + - port: 8081 + name: http + nodePort: 30848 + - port: 5005 + name: debug + selector: + app: dubhe-serving-gateway +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-serving-gateway + labels: + account: dubhe-serving-gateway +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-serving-gateway-v1 + labels: + app: dubhe-serving-gateway + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-serving-gateway + version: v1 + template: + metadata: + labels: + app: dubhe-serving-gateway + version: v1 + spec: + serviceAccountName: dubhe-serving-gateway + containers: + - name: dubhe-serving-gateway + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-serving-gateway-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-serving-gateway-dump.hprof" + ports: + - containerPort: 8081 + - containerPort: 5505 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-serving.yaml b/dubhe-server/deploy/cloud/server-dubhe-serving.yaml new file mode 100644 index 0000000..5b4b819 --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-serving.yaml @@ -0,0 +1,81 @@ +################################################################################################## +# dubhe-serving +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-serving + labels: + app: dubhe-serving + service: dubhe-serving +spec: + type: NodePort + ports: + - port: 8898 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-serving +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-serving + labels: + account: dubhe-serving +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-serving-v1 + labels: + app: dubhe-serving + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-serving + version: v1 + template: + metadata: + labels: + app: dubhe-serving + version: v1 + spec: + serviceAccountName: dubhe-serving + containers: + - name: dubhe-serving + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-serving-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-serving-dump.hprof" + ports: + - containerPort: 8898 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe-train.yaml b/dubhe-server/deploy/cloud/server-dubhe-train.yaml new file mode 100644 index 0000000..5bc190d --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe-train.yaml @@ -0,0 +1,78 @@ +################################################################################################## +# dubhe-train +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-train + labels: + app: dubhe-train + service: dubhe-train +spec: + type: NodePort + ports: + - port: 8890 + name: http + selector: + app: dubhe-train +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-train + labels: + account: dubhe-train +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-train-v1 + labels: + app: dubhe-train + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-train + version: v1 + template: + metadata: + labels: + app: dubhe-train + version: v1 + spec: + serviceAccountName: dubhe-train + containers: + - name: dubhe-train + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-train-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-train-dump.hprof" + ports: + - containerPort: 8890 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-dubhe.yaml b/dubhe-server/deploy/cloud/server-dubhe.yaml new file mode 100644 index 0000000..4eff9a9 --- /dev/null +++ b/dubhe-server/deploy/cloud/server-dubhe.yaml @@ -0,0 +1,1425 @@ +################################################################################################## +# admin +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: admin + labels: + app: admin + service: admin +spec: + type: NodePort + ports: + - port: 8870 + name: http + selector: + app: admin +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: admin + labels: + account: admin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: admin-v1 + labels: + app: admin + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: admin + version: v1 + template: + metadata: + labels: + app: admin + version: v1 + spec: + serviceAccountName: admin + containers: + - name: admin + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "admin-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/admin-dump.hprof" + ports: + - containerPort: 8870 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" + +################################################################################################## +# auth +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: auth + labels: + app: auth + service: auth +spec: + type: NodePort + ports: + - port: 8866 + name: http + selector: + app: auth +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: auth + labels: + account: auth +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth-v1 + labels: + app: auth + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: auth + version: v1 + template: + metadata: + labels: + app: auth + version: v1 + spec: + serviceAccountName: auth + containers: + - name: auth + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "auth-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/auth-dump.hprof" + ports: + - containerPort: 8866 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# demo-client service +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: demo-client + labels: + app: demo-client + service: demo-client +spec: + type: NodePort + ports: + - port: 8861 + name: http + selector: + app: demo-client +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: demo-client + labels: + account: demo-client +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demo-client-v1 + labels: + app: demo-client + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: demo-client + version: v1 + template: + metadata: + labels: + app: demo-client + version: v1 + spec: + serviceAccountName: demo-client + containers: + - name: demo-client + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "demo-client-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/demo-client-dump.hprof" + ports: + - containerPort: 8861 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# demo-provider service +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: demo-provider + labels: + app: demo-provider + service: demo-provider +spec: + type: NodePort + ports: + - port: 8860 + name: http + selector: + app: demo-provider +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: demo-provider + labels: + account: demo-provider +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demo-provider-v1 + labels: + app: demo-provider + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: demo-provider + version: v1 + template: + metadata: + labels: + app: demo-provider + version: v1 + spec: + serviceAccountName: demo-provider + containers: + - name: demo-provider + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "demo-provider-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/demo-provider-dump.hprof" + ports: + - containerPort: 8860 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-algorithm +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-algorithm + labels: + app: dubhe-algorithm + service: dubhe-algorithm +spec: + type: NodePort + ports: + - port: 8889 + name: http + selector: + app: dubhe-algorithm +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-algorithm + labels: + account: dubhe-algorithm +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-algorithm-v1 + labels: + app: dubhe-algorithm + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-algorithm + version: v1 + template: + metadata: + labels: + app: dubhe-algorithm + version: v1 + spec: + serviceAccountName: dubhe-algorithm + containers: + - name: dubhe-algorithm + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-algorithm-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-algorithm-dump.hprof" + ports: + - containerPort: 8889 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-data +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-data + labels: + app: dubhe-data + service: dubhe-data +spec: + type: NodePort + ports: + - port: 8823 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-data +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-data + labels: + account: dubhe-data +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-data-v1 + labels: + app: dubhe-data + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-data + version: v1 + template: + metadata: + labels: + app: dubhe-data + version: v1 + spec: + serviceAccountName: dubhe-data + containers: + - name: dubhe-data + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-data-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms4096m -Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-data-dump.hprof" + ports: + - containerPort: 8823 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-image +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-image + labels: + app: dubhe-image + service: dubhe-image +spec: + type: NodePort + ports: + - port: 8822 + name: http + selector: + app: dubhe-image +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-image + labels: + account: dubhe-image +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-image-v1 + labels: + app: dubhe-image + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-image + version: v1 + template: + metadata: + labels: + app: dubhe-image + version: v1 + spec: + serviceAccountName: dubhe-image + containers: + - name: dubhe-image + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-image-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-image-dump.hprof" + ports: + - containerPort: 8822 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-k8s +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-k8s + labels: + app: dubhe-k8s + service: dubhe-k8s +spec: + type: NodePort + ports: + - port: 8960 + name: http + selector: + app: dubhe-k8s +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-k8s + labels: + account: dubhe-k8s +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-k8s-v1 + labels: + app: dubhe-k8s + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-k8s + version: v1 + template: + metadata: + labels: + app: dubhe-k8s + version: v1 + spec: + serviceAccountName: dubhe-k8s + containers: + - name: dubhe-k8s + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-k8s-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-k8s-dump.hprof" + ports: + - containerPort: 8960 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-measure +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-measure + labels: + app: dubhe-measure + service: dubhe-measure +spec: + type: NodePort + ports: + - port: 8821 + name: http + selector: + app: dubhe-measure +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-measure + labels: + account: dubhe-measure +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-measure-v1 + labels: + app: dubhe-measure + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-measure + version: v1 + template: + metadata: + labels: + app: dubhe-measure + version: v1 + spec: + serviceAccountName: dubhe-measure + containers: + - name: dubhe-measure + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-measure-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-measure-dump.hprof" + ports: + - containerPort: 8821 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-model +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-model + labels: + app: dubhe-model + service: dubhe-model +spec: + type: NodePort + ports: + - port: 8888 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-model +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-model + labels: + account: dubhe-model +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-model-v1 + labels: + app: dubhe-model + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-model + version: v1 + template: + metadata: + labels: + app: dubhe-model + version: v1 + spec: + serviceAccountName: dubhe-model + containers: + - name: dubhe-model + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-model-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-model-dump.hprof" + ports: + - containerPort: 8888 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-notebook +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-notebook + labels: + app: dubhe-notebook + service: dubhe-notebook +spec: + type: NodePort + ports: + - port: 8863 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-notebook +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-notebook + labels: + account: dubhe-notebook +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-notebook-v1 + labels: + app: dubhe-notebook + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-notebook + version: v1 + template: + metadata: + labels: + app: dubhe-notebook + version: v1 + spec: + serviceAccountName: dubhe-notebook + containers: + - name: dubhe-notebook + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-notebook-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-notebook-dump.hprof" + ports: + - containerPort: 8863 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-data-dcm +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-data-dcm + labels: + app: dubhe-data-dcm + service: dubhe-data-dcm +spec: + type: NodePort + ports: + - port: 8011 + name: http + selector: + app: dubhe-data-dcm +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-data-dcm + labels: + account: dubhe-data-dcm +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-data-dcm-v1 + labels: + app: dubhe-data-dcm + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-data-dcm + version: v1 + template: + metadata: + labels: + app: dubhe-data-dcm + version: v1 + spec: + serviceAccountName: dubhe-data-dcm + containers: + - name: dubhe-data-dcm + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-data-dcm-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-data-dcm-dump.hprof" + ports: + - containerPort: 8011 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-data-task +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-data-task + labels: + app: dubhe-data-task + service: dubhe-data-task +spec: + type: NodePort + ports: + - port: 8801 + name: http + selector: + app: dubhe-data-task +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-data-task + labels: + account: dubhe-data-task +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-data-task-v1 + labels: + app: dubhe-data-task + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-data-task + version: v1 + template: + metadata: + labels: + app: dubhe-data-task + version: v1 + spec: + serviceAccountName: dubhe-data-task + containers: + - name: dubhe-data-task + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-data-task-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-data-task-dump.hprof" + ports: + - containerPort: 8801 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-train +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-train + labels: + app: dubhe-train + service: dubhe-train +spec: + type: NodePort + ports: + - port: 8890 + name: http + selector: + app: dubhe-train +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-train + labels: + account: dubhe-train +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-train-v1 + labels: + app: dubhe-train + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-train + version: v1 + template: + metadata: + labels: + app: dubhe-train + version: v1 + spec: + serviceAccountName: dubhe-train + containers: + - name: dubhe-train + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-train-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-train-dump.hprof" + ports: + - containerPort: 8890 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-optimize +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-optimize + labels: + app: dubhe-optimize + service: dubhe-optimize +spec: + type: NodePort + ports: + - port: 8899 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-optimize +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-optimize + labels: + account: dubhe-optimize +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-optimize-v1 + labels: + app: dubhe-optimize + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-optimize + version: v1 + template: + metadata: + labels: + app: dubhe-optimize + version: v1 + spec: + serviceAccountName: dubhe-optimize + containers: + - name: dubhe-optimize + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-optimize-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-optimize-dump.hprof" + ports: + - containerPort: 8899 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-serving +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-serving + labels: + app: dubhe-serving + service: dubhe-serving +spec: + type: NodePort + ports: + - port: 8898 + name: http + - port: 5005 + name: debug + selector: + app: dubhe-serving +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-serving + labels: + account: dubhe-serving +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-serving-v1 + labels: + app: dubhe-serving + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-serving + version: v1 + template: + metadata: + labels: + app: dubhe-serving + version: v1 + spec: + serviceAccountName: dubhe-serving + containers: + - name: dubhe-serving + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-serving-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-serving-dump.hprof" + ports: + - containerPort: 8898 + - containerPort: 5005 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# dubhe-serving-gateway +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: dubhe-serving-gateway + labels: + app: dubhe-serving-gateway + service: dubhe-serving-gateway +spec: + type: NodePort + ports: + - port: 8081 + name: http + nodePort: 30848 + - port: 5005 + name: debug + selector: + app: dubhe-serving-gateway +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dubhe-serving-gateway + labels: + account: dubhe-serving-gateway +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dubhe-serving-gateway-v1 + labels: + app: dubhe-serving-gateway + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: dubhe-serving-gateway + version: v1 + template: + metadata: + labels: + app: dubhe-serving-gateway + version: v1 + spec: + serviceAccountName: dubhe-serving-gateway + containers: + - name: dubhe-serving-gateway + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "dubhe-serving-gateway-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/dubhe-serving-gateway-dump.hprof" + ports: + - containerPort: 8081 + - containerPort: 5505 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" +################################################################################################## +# gateway +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: gateway + labels: + app: gateway + service: gateway +spec: + type: NodePort + ports: + - port: 8800 + name: http + nodePort: gatewayNodePort + selector: + app: gateway +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gateway + labels: + account: gateway +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gateway-v1 + labels: + app: gateway + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: gateway + version: v1 + template: + metadata: + labels: + app: gateway + version: v1 + spec: + serviceAccountName: gateway + containers: + - name: gateway + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "gateway-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/gateway-dump.hprof" + ports: + - containerPort: 8800 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/deploy/cloud/server-gateway.yaml b/dubhe-server/deploy/cloud/server-gateway.yaml new file mode 100644 index 0000000..6b598eb --- /dev/null +++ b/dubhe-server/deploy/cloud/server-gateway.yaml @@ -0,0 +1,79 @@ +################################################################################################## +# gateway +################################################################################################## +--- +apiVersion: v1 +kind: Service +metadata: + name: gateway + labels: + app: gateway + service: gateway +spec: + type: NodePort + ports: + - port: 8800 + name: http + nodePort: gatewayNodePort + selector: + app: gateway +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gateway + labels: + account: gateway +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gateway-v1 + labels: + app: gateway + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: gateway + version: v1 + template: + metadata: + labels: + app: gateway + version: v1 + spec: + serviceAccountName: gateway + containers: + - name: gateway + image: harbor.test.com/dubhe/dubhe-spring-cloud-k8s:env-value + imagePullPolicy: Always + env: + - name: JAR_BALL + value: "gateway-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=env-value" + - name: JVM_PARAM + value: "-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=containerLogPath/gateway-dump.hprof" + ports: + - containerPort: 8800 + volumeMounts: + - mountPath: "fsPath" + name: "fs-volume" + readOnly: false + - mountPath: "containerLogPath" + name: "log-volume" + readOnly: false + - name: "dockersock" + mountPath: "/var/run/docker.sock" + volumes: + - name: "fs-volume" + hostPath: + path: "fsPath" + type: "Directory" + - name: "log-volume" + hostPath: + path: "hostLogPath" + type: "DirectoryOrCreate" + - name: "dockersock" + hostPath: + path: "/var/run/docker.sock" \ No newline at end of file diff --git a/dubhe-server/dubhe-algorithm/pom.xml b/dubhe-server/dubhe-algorithm/pom.xml new file mode 100644 index 0000000..966a761 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/pom.xml @@ -0,0 +1,125 @@ + + + + server + org.dubhe + 0.0.1-SNAPSHOT + + 4.0.0 + dubhe-algorithm + 0.0.1-SNAPSHOT + algorithm 算法管理 + Dubhe algorithm + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + org.dubhe.biz + data-permission + ${org.dubhe.biz.data-permission.version} + + + org.dubhe.biz + data-permission + ${org.dubhe.biz.data-permission.version} + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + org.dubhe.biz + file + ${org.dubhe.biz.file.version} + + + + org.dubhe.cloud + swagger + ${org.dubhe.cloud.swagger.version} + + + + org.dubhe.cloud + auth-config + ${org.dubhe.cloud.auth-config.version} + + + + org.dubhe.cloud + remote-call + ${org.dubhe.cloud.remote-call.version} + + + + org.dubhe.cloud + registration + ${org.dubhe.cloud.registration.version} + + + + org.dubhe.cloud + configuration + ${org.dubhe.cloud.configuration.version} + + + + org.dubhe + common-k8s + ${org.dubhe.common-k8s.version} + + + + org.dubhe + common-recycle + ${org.dubhe.common-recycle.version} + + + + org.dubhe.cloud + unit-test + ${org.dubhe.cloud.unit-test.version} + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + true + exec + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + true + src/main/resources + + + + + \ No newline at end of file diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/AlgorithmApplication.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/AlgorithmApplication.java new file mode 100644 index 0000000..ae0e7a2 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/AlgorithmApplication.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @description alrigothm启动类 + * @date 2020-12-18 + */ +@SpringBootApplication(scanBasePackages = "org.dubhe") +@MapperScan(basePackages = {"org.dubhe.**.dao"}) +@EnableScheduling +@EnableAsync +public class AlgorithmApplication { + public static void main(String[] args) { + SpringApplication.run(AlgorithmApplication.class, args); + } +} \ No newline at end of file diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/async/TrainAlgorithmUploadAsync.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/async/TrainAlgorithmUploadAsync.java new file mode 100644 index 0000000..9f66d51 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/async/TrainAlgorithmUploadAsync.java @@ -0,0 +1,119 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.async; + +import org.dubhe.algorithm.client.NoteBookClient; +import org.dubhe.algorithm.constant.AlgorithmConstant; +import org.dubhe.algorithm.dao.PtTrainAlgorithmMapper; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmCreateDTO; +import org.dubhe.algorithm.domain.entity.PtTrainAlgorithm; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.NoteBookAlgorithmUpdateDTO; +import org.dubhe.biz.base.enums.AlgorithmStatusEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.enums.BizPathEnum; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.k8s.utils.K8sNameTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Arrays; + +/** + * @description 异步上传算法 + * @date 2020-08-10 + */ +@Component +public class TrainAlgorithmUploadAsync { + + @Autowired + private K8sNameTool k8sNameTool; + + @Autowired + private NoteBookClient noteBookClient; + + @Autowired + private PtTrainAlgorithmMapper trainAlgorithmMapper; + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + /** + * 异步任务创建算法 + * + * @param user 当前登录用户信息 + * @param ptTrainAlgorithm 算法信息 + * @param trainAlgorithmCreateDTO 创建算法条件 + */ + @Async(AlgorithmConstant.ALGORITHM_EXECUTOR) + public void createTrainAlgorithm(UserContext user, PtTrainAlgorithm ptTrainAlgorithm, PtTrainAlgorithmCreateDTO trainAlgorithmCreateDTO) { + String path = fileStoreApi.getBucket() + trainAlgorithmCreateDTO.getCodeDir(); + //校验创建算法来源(true:由fork创建算法,false:其它创建算法方式),若为true则拷贝预置算法文件至新路径 + if (trainAlgorithmCreateDTO.getFork()) { + //生成算法相对路径 + String algorithmPath = k8sNameTool.getPath(BizPathEnum.ALGORITHM, user.getId()); + //拷贝预置算法文件夹 + boolean copyResult = fileStoreApi.copyPath(fileStoreApi.getRootDir() + path, fileStoreApi.getRootDir() + fileStoreApi.getBucket() + algorithmPath); + if (!copyResult) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "The user {} copied the preset algorithm path {} successfully", user.getUsername(), path); + updateTrainAlgorithm(ptTrainAlgorithm, trainAlgorithmCreateDTO, false); + throw new BusinessException("内部错误"); + } + + ptTrainAlgorithm.setCodeDir(algorithmPath); + + //修改算法上传状态 + updateTrainAlgorithm(ptTrainAlgorithm, trainAlgorithmCreateDTO, true); + + } else { + updateTrainAlgorithm(ptTrainAlgorithm, trainAlgorithmCreateDTO, true); + } + } + + + /** + * 更新上传算法状态 + * + * @param ptTrainAlgorithm 算法信息 + * @param trainAlgorithmCreateDTO 创建算法的条件 + * @param flag 创建算法是否成功(true:成功,false:失败) + */ + public void updateTrainAlgorithm(PtTrainAlgorithm ptTrainAlgorithm, PtTrainAlgorithmCreateDTO trainAlgorithmCreateDTO, boolean flag) { + + LogUtil.info(LogEnum.BIZ_ALGORITHM, "async update algorithmPath by algorithmId:{} and update noteBook by noteBookId:{}", ptTrainAlgorithm.getId(), trainAlgorithmCreateDTO.getNoteBookId()); + if (flag) { + ptTrainAlgorithm.setAlgorithmStatus(AlgorithmStatusEnum.SUCCESS.getCode()); + //更新fork算法新路径 + trainAlgorithmMapper.updateById(ptTrainAlgorithm); + //保存算法根据notbookId更新算法id + if (trainAlgorithmCreateDTO.getNoteBookId() != null) { + LogUtil.info(LogEnum.BIZ_ALGORITHM, "Save algorithm Update algorithm ID :{} according to notBookId:{}", trainAlgorithmCreateDTO.getNoteBookId(), ptTrainAlgorithm.getId()); + NoteBookAlgorithmUpdateDTO noteBookAlgorithmUpdateDTO = new NoteBookAlgorithmUpdateDTO(); + noteBookAlgorithmUpdateDTO.setAlgorithmId(ptTrainAlgorithm.getId()); + noteBookAlgorithmUpdateDTO.setNotebookIdList(Arrays.asList(trainAlgorithmCreateDTO.getNoteBookId())); + noteBookClient.updateNoteBookAlgorithm(noteBookAlgorithmUpdateDTO); + } + } else { + ptTrainAlgorithm.setAlgorithmStatus(AlgorithmStatusEnum.FAIL.getCode()); + trainAlgorithmMapper.updateById(ptTrainAlgorithm); + } + } +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/ImageClient.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/ImageClient.java new file mode 100644 index 0000000..490c9eb --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/ImageClient.java @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.client; + +import org.dubhe.algorithm.client.fallback.ImageClientFallback; +import org.dubhe.biz.base.constant.ApplicationNameConst; +import org.dubhe.biz.base.dto.PtImageQueryUrlDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * @description image远程服务调用接口 + * @date 2020-12-14 + */ +@FeignClient(value = ApplicationNameConst.SERVER_IMAGE, contextId = "imageClient", fallback = ImageClientFallback.class) +public interface ImageClient { + + /** + * 远程获取镜像URL + * + * @param ptImageQueryUrlDTO 查询镜像路径DTO + * @return string 镜像url + */ + @GetMapping(value = "/ptImage/imageUrl") + DataResponseBody getImageUrl(@SpringQueryMap PtImageQueryUrlDTO ptImageQueryUrlDTO); +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/NoteBookClient.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/NoteBookClient.java new file mode 100644 index 0000000..ed33ffe --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/NoteBookClient.java @@ -0,0 +1,54 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.client; + +import org.dubhe.algorithm.client.fallback.NoteBookClientFallback; +import org.dubhe.biz.base.constant.ApplicationNameConst; +import org.dubhe.biz.base.dto.NoteBookAlgorithmQueryDTO; +import org.dubhe.biz.base.dto.NoteBookAlgorithmUpdateDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; + +import java.util.List; + +/** + * @description notebook远程服务调用接口 + * @date 2020-12-14 + */ +@FeignClient(value = ApplicationNameConst.SERVER_NOTEBOOK, contextId = "noteBookClient", fallback = NoteBookClientFallback.class) +public interface NoteBookClient { + + /** + * 更新notebook算法ID + * + * @param noteBookAlgorithmUpdateDTO 更新notebook算法ID DTO + */ + @PutMapping(value = "/notebooks/algorithm") + void updateNoteBookAlgorithm(NoteBookAlgorithmUpdateDTO noteBookAlgorithmUpdateDTO); + + /** + * 获取notebook算法ID + * + * @param noteBookAlgorithmQueryDTO 获取notebook算法ID DTO + * @return 算法ID + */ + @GetMapping(value = "/notebooks/algorithm") + DataResponseBody> getNoteBookIdByAlgorithm(@SpringQueryMap NoteBookAlgorithmQueryDTO noteBookAlgorithmQueryDTO); +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/ImageClientFallback.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/ImageClientFallback.java new file mode 100644 index 0000000..595d8a8 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/ImageClientFallback.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.client.fallback; + + +import org.dubhe.algorithm.client.ImageClient; +import org.dubhe.biz.base.dto.PtImageQueryUrlDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.stereotype.Component; + +/** + * @description image远程服务调用类熔断 + * @date 2020-12-14 + */ +@Component +public class ImageClientFallback implements ImageClient { + + @Override + public DataResponseBody getImageUrl(PtImageQueryUrlDTO ptImageQueryUrlDTO) { + return DataResponseFactory.failed("call image server getImageUrl error"); + } +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/NoteBookClientFallback.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/NoteBookClientFallback.java new file mode 100644 index 0000000..4d60941 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/client/fallback/NoteBookClientFallback.java @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.client.fallback; + +import org.dubhe.algorithm.client.NoteBookClient; +import org.dubhe.biz.base.dto.NoteBookAlgorithmQueryDTO; +import org.dubhe.biz.base.dto.NoteBookAlgorithmUpdateDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.stereotype.Component; + +/** + * @description notebook远程服务调用类熔断 + * @date 2020-12-14 + */ +@Component +public class NoteBookClientFallback implements NoteBookClient { + + @Override + public void updateNoteBookAlgorithm(NoteBookAlgorithmUpdateDTO noteBookAlgorithmUpdateDTO) { + DataResponseFactory.failed("call notebook server updateNoteBookAlgorithm error"); + } + + @Override + public DataResponseBody getNoteBookIdByAlgorithm(NoteBookAlgorithmQueryDTO noteBookAlgorithmQueryDTO) { + return DataResponseFactory.failed("call notebook server getNoteBookIdByAlgorithm error"); + } +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/config/AlgorithmPoolConfig.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/config/AlgorithmPoolConfig.java new file mode 100644 index 0000000..aac88eb --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/config/AlgorithmPoolConfig.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.config; + +import org.dubhe.algorithm.constant.AlgorithmConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @description 线程池配置类 + * @date 2020-07-17 + */ +@Configuration +public class AlgorithmPoolConfig implements AsyncConfigurer { + + @Value("${basepool.corePoolSize:40}") + private Integer corePoolSize; + @Value("${basepool.maximumPoolSize:60}") + private Integer maximumPoolSize; + @Value("${basepool.keepAliveTime:120}") + private Integer keepAliveTime; + @Value("${basepool.blockQueueSize:20}") + private Integer blockQueueSize; + + /** + * 算法任务异步处理线程池 + * @return Executor 线程实例 + */ + @Bean(AlgorithmConstant.ALGORITHM_EXECUTOR) + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + //核心线程数 + taskExecutor.setCorePoolSize(corePoolSize); + taskExecutor.setAllowCoreThreadTimeOut(true); + //最大线程数 + taskExecutor.setMaxPoolSize(maximumPoolSize); + //超时时间 + taskExecutor.setKeepAliveSeconds(keepAliveTime); + //配置队列大小 + taskExecutor.setQueueCapacity(blockQueueSize); + //配置线程池前缀 + taskExecutor.setThreadNamePrefix("async-algorithm-"); + //拒绝策略 + taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); + taskExecutor.initialize(); + return taskExecutor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "开始捕获算法管理异步任务异常信息-----》》》"); + return (ex, method, params) -> { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "算法管理方法名{}的异步任务执行失败,参数信息:{},异常信息:{}", method.getName(), params, ex); + }; + } +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/AlgorithmConstant.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/AlgorithmConstant.java new file mode 100644 index 0000000..3405fb5 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/AlgorithmConstant.java @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.constant; + +/** + * @description 常量 + * @date 2020-04-10 + */ +public class AlgorithmConstant { + + + /** + * 排序规则 + */ + public static final String SORT_ASC = "asc"; + + public static final String SORT_DESC = "desc"; + + /** + * ZIP压缩文件后缀 + */ + public static final String COMPRESS_ZIP = ".zip"; + + /** + * 可推理文件后缀 + */ + public static final String COMPRESS_PY = ".py"; + + + /** + * id + **/ + public static final String ID = "id"; + + public final static String ALGORITHM_EXECUTOR = "algorithmExecutor"; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/TrainAlgorithmConfig.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/TrainAlgorithmConfig.java new file mode 100644 index 0000000..960205c --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/TrainAlgorithmConfig.java @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.constant; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @description 算法常量 + * @date 2020-06-02 + */ +@Data +@Component +@ConfigurationProperties(prefix = "train-algorithm") +public class TrainAlgorithmConfig { + + /** + * 是否输出训练结果 + */ + private Boolean isTrainModelOut; + + /** + * 是否输出训练信息 + */ + private Boolean isTrainOut; + + /** + * 是否输出可视化日志 + */ + private Boolean isVisualizedLog; + + /** + * 设置默认算法来源(1为我的算法,2为预置算法) + */ + private Integer algorithmSource; + + /** + * 设置fork默认值(fork:创建算法来源) + */ + private Boolean fork; + + /** + * 设置inference默认值(inference:上传算法是否支持推理) + */ + private Boolean inference; +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/UserAuxiliaryInfoConstant.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/UserAuxiliaryInfoConstant.java new file mode 100644 index 0000000..c59c7d7 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/constant/UserAuxiliaryInfoConstant.java @@ -0,0 +1,31 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.constant; + +import lombok.Data; + +/** + * @description 算法用途 + * @date 2020-06-23 + */ +@Data +public class UserAuxiliaryInfoConstant { + + public static final String ALGORITHM_USAGE = "algorithem_usage"; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmMapper.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmMapper.java new file mode 100644 index 0000000..ba59c52 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmMapper.java @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.dubhe.biz.base.annotation.DataPermission; +import org.dubhe.algorithm.domain.entity.PtTrainAlgorithm; + +import java.util.List; +import java.util.Set; + +/** + * @description 训练算法Mapper + * @date 2020-04-27 + */ +@DataPermission(ignoresMethod = {"insert"}) +public interface PtTrainAlgorithmMapper extends BaseMapper { + + /** + * 根据算法id查询算法信息 + * @param id 算法id + * @return PtTrainAlgorithm 算法信息 + */ + @Select("select * from pt_train_algorithm where id= #{id}") + PtTrainAlgorithm selectAllById(@Param("id") Long id); + + /** + * 根据算法id集合查询对应的算法信息 + * @param ids 算法集合id + * @return List 算法信息集合 + */ + @Select({ + "" + }) + List selectAllBatchIds(@Param("ids") Set ids); + + /** + * 算法还原 + * @param id 算法id + * @param deleteFlag 删除状态 + * @return 数量 + */ + @Update("update pt_train_algorithm set deleted = #{deleteFlag} where id = #{id}") + int updateStatusById(@Param("id") Long id, @Param("deleteFlag") boolean deleteFlag); +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmUsageMapper.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmUsageMapper.java new file mode 100644 index 0000000..58e3e8a --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/dao/PtTrainAlgorithmUsageMapper.java @@ -0,0 +1,32 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.dubhe.algorithm.domain.entity.PtTrainAlgorithmUsage; +import org.dubhe.biz.base.annotation.DataPermission; + +/** + * + * @description 用户辅助信息Mapper 接口 + * @date 2020-06-23 + */ +@DataPermission(ignoresMethod = "insert") +public interface PtTrainAlgorithmUsageMapper extends BaseMapper { + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtModelAlgorithmCreateDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtModelAlgorithmCreateDTO.java new file mode 100644 index 0000000..88cac6a --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtModelAlgorithmCreateDTO.java @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.algorithm.utils.TrainUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 模型优化上传算法入参 + * @date 2021-01-06 + */ +@Data +@Accessors(chain = true) +public class PtModelAlgorithmCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "算法名称,输入长度不能超过32个字符", required = true) + @NotBlank(message = "算法名称不能为空") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "算法名称-输入长度不能超过32个字符") + @Pattern(regexp = TrainUtil.REGEXP, message = "算法名称支持字母、数字、汉字、英文横杠和下划线") + private String name; + + @ApiModelProperty(value = "代码目录(路径规则:/algorithm-manage/{userId}/{YYYYMMDDhhmmssSSS+四位随机数}/用户上传的算法具体文件(zip文件)名称或从notebook跳转时为/notebook/{userId}/{YYYYMMDDhhmmssSSS+四位随机数}/)", required = true) + @NotBlank(message = "代码目录不能为空") + @Length(max = MagicNumConstant.ONE_HUNDRED_TWENTY_EIGHT, message = "代码目录-输入长度不能超过128个字符") + private String path; + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmCreateDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmCreateDTO.java new file mode 100644 index 0000000..f1dc9dd --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmCreateDTO.java @@ -0,0 +1,98 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.dto; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.algorithm.utils.TrainUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @desciption 创建算法条件 + * @date 2020-04-29 + */ +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "算法名称,输入长度不能超过32个字符", required = true) + @NotBlank(message = "算法名称不能为空") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "算法名称-输入长度不能超过32个字符") + @Pattern(regexp = TrainUtil.REGEXP, message = "算法名称支持字母、数字、汉字、英文横杠和下划线") + private String algorithmName; + + @ApiModelProperty("算法描述,输入长度不能超过256个字符") + @Length(max = MagicNumConstant.INTEGER_TWO_HUNDRED_AND_FIFTY_FIVE, message = "算法描述-输入长度不能超过256个字符") + private String description; + + @ApiModelProperty(value = "镜像版本") + private String imageTag; + + @ApiModelProperty(value = "镜像名称") + private String imageName; + + @ApiModelProperty(value = "创建算法来源(true:由fork创建算法,false:其它创建算法方式,null:默认false)") + private Boolean fork; + + @ApiModelProperty(value = "上传算法是否支持推理(true:可推理,false:不可推理,null:默认false)") + private Boolean inference; + + @ApiModelProperty(value = "代码目录(路径规则:/algorithm-manage/{userId}/{YYYYMMDDhhmmssSSS+四位随机数}/用户上传的算法具体文件(zip文件)名称或从notebook跳转时为/notebook/{userId}/{YYYYMMDDhhmmssSSS+四位随机数}/)", required = true) + @NotBlank(message = "代码目录不能为空") + @Length(max = MagicNumConstant.ONE_HUNDRED_TWENTY_EIGHT, message = "代码目录-输入长度不能超过128个字符") + private String codeDir; + + @ApiModelProperty(value = "运行命令,管理员使用") + @Length(max = MagicNumConstant.ONE_HUNDRED_TWENTY_EIGHT, message = "运行命令-输入长度不能超过128个字符") + private String runCommand; + + @ApiModelProperty("运行参数(算法来源为我的算法时为调优参数,算法来源为预置算法时为运行参数),管理员使用") + private JSONObject runParams; + + @ApiModelProperty("算法来源(1为我的算法,2为预置算法),管理员使用") + private Integer algorithmSource; + + @ApiModelProperty("算法用途,输入长度不能超过128个字符") + @Length(max = MagicNumConstant.ONE_HUNDRED_TWENTY_EIGHT, message = "算法用途-输入长度不能超过128个字符") + private String algorithmUsage; + + @ApiModelProperty("是否输出训练结果,不填则默认为true") + private Boolean isTrainModelOut; + + @ApiModelProperty("是否输出训练信息,不填则默认为true") + private Boolean isTrainOut; + + @ApiModelProperty("是否输出可视化日志,不填则默认为false") + private Boolean isVisualizedLog; + + @ApiModelProperty("noteBookId") + private Long noteBookId; + + @ApiModelProperty("资源拥有者ID") + private Long originUserId; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmDeleteDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmDeleteDTO.java new file mode 100644 index 0000000..4ffc4d2 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmDeleteDTO.java @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Set; + +/** + * @description 算法删除 + * @date 2020-07-02 + */ +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为空") + private Set ids; + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmQueryDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmQueryDTO.java new file mode 100644 index 0000000..6f5e9d7 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmQueryDTO.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.dubhe.algorithm.utils.TrainUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.db.annotation.Query; +import org.dubhe.biz.db.base.PageQueryBase; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import java.io.Serializable; + +/** + * @description 查询算法条件 + * @date 2020-04-29 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmQueryDTO extends PageQueryBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "算法来源(1为我的算法, 2为预置算法)") + @Min(value = TrainUtil.NUMBER_ONE, message = "算法来源错误") + @Max(value = TrainUtil.NUMBER_TWO, message = "算法来源错误") + @Query(propName = "algorithm_source", type = Query.Type.EQ) + private Integer algorithmSource; + + @ApiModelProperty(value = "算法名称或者id") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "算法名称或者id有误") + private String algorithmName; + + @ApiModelProperty(value = "算法用途") + private String algorithmUsage; + + @ApiModelProperty(value = "上传算法是否支持推理(true:可推理,false:不可推理)") + private Boolean inference; +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUpdateDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUpdateDTO.java new file mode 100644 index 0000000..adc57ff --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUpdateDTO.java @@ -0,0 +1,65 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.algorithm.utils.TrainUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 修改算法 + * @date 2020-06-19 + */ +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmUpdateDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为null") + @Min(value = TrainUtil.NUMBER_ONE, message = "id必须大于1") + private Long id; + + @ApiModelProperty(value = "算法名称,输入长度不能超过32个字符") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "算法名称-输入长度不能超过32个字符") + @Pattern(regexp = TrainUtil.REGEXP, message = "算法名称支持字母、数字、汉字、英文横杠和下划线") + private String algorithmName; + + @ApiModelProperty("算法描述,输入长度不能超过256个字符") + @Length(max = MagicNumConstant.INTEGER_TWO_HUNDRED_AND_FIFTY_FIVE, message = "算法描述-输入长度不能超过256个字符") + private String description; + + @ApiModelProperty("算法用途,输入长度不能超过128个字符") + @Length(max = MagicNumConstant.ONE_HUNDRED_TWENTY_EIGHT, message = "算法用途-输入长度不能超过128个字符") + private String algorithmUsage; + + @ApiModelProperty("是否输出训练信息") + private Boolean isTrainOut; + + @ApiModelProperty("是否输出可视化日志") + private Boolean isVisualizedLog; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageCreateDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageCreateDTO.java new file mode 100644 index 0000000..7096c4a --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageCreateDTO.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.algorithm.utils.TrainUtil; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @description 创建算法用途条件 + * @date 2020-06-23 + */ +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmUsageCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "名称输入长度不能超过32个字符", required = true) + @NotBlank(message = "名称不能为空") + @Length(max = MagicNumConstant.THIRTY_TWO, message = "名称-输入长度不能超过32个字符") + @Pattern(regexp = TrainUtil.REGEXP, message = "名称支持字母、数字、汉字、英文横杠和下划线") + private String auxInfo; + + @ApiModelProperty(value = "类型", hidden = true) + private String type; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageDeleteDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageDeleteDTO.java new file mode 100644 index 0000000..d6f04b6 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageDeleteDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 算法用途删除 + * @date 2020-07-02 + */ +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmUsageDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为空") + private Long[] ids; + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageQueryDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageQueryDTO.java new file mode 100644 index 0000000..e6cd4a2 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageQueryDTO.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.base.PageQueryBase; + +import javax.validation.constraints.NotNull; + +/** + * @description 查询算法用途条件 + * @date 2020-06-23 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmUsageQueryDTO extends PageQueryBase { + + @ApiModelProperty(value = "类型", hidden = true) + private String type; + + @ApiModelProperty(value = "是否包含默认值 0:不包含 1包含", required = true) + @NotNull + private Boolean isContainDefault; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageUpdateDTO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageUpdateDTO.java new file mode 100644 index 0000000..2483ac3 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/dto/PtTrainAlgorithmUsageUpdateDTO.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dubhe.algorithm.utils.TrainUtil; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 算法用途修改 + * @date 2020-06-23 + */ +@Data +@Accessors(chain = true) +public class PtTrainAlgorithmUsageUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为null") + @Min(value = TrainUtil.NUMBER_ONE, message = "id必须大于1") + private Long id; + + @ApiModelProperty("描述, 长度不能超过20个字符") + @Length(max = TrainUtil.NUMBER_TWENTY, message = "名称长度不能超过20个字符") + private String auxInfo; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithm.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithm.java new file mode 100644 index 0000000..cb55ccf --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithm.java @@ -0,0 +1,147 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.entity; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; + +import javax.validation.constraints.NotNull; + +/** + * @description 算法 + * @date 2020-04-29 + */ + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName(value = "pt_train_algorithm", autoResultMap = true) +@Accessors(chain = true) +public class PtTrainAlgorithm extends BaseEntity { + + /** + * 主键 + */ + @TableId(value = "id", type = IdType.AUTO) + @NotNull(groups = {Update.class}) + private Long id; + + /** + * 算法名称 + */ + @TableField(value = "algorithm_name") + private String algorithmName; + + /** + * 算法描述 + */ + @TableField(value = "description") + private String description; + + /** + * 算法来源(1为我的算法,2为预置算法) + */ + @TableField(value = "algorithm_source") + private Integer algorithmSource; + + /** + * 环境镜像名称 + */ + @TableField(value = "image_name") + private String imageName; + + /** + * 代码目录 + */ + @TableField(value = "code_dir") + private String codeDir; + + /** + * 运行命令 + */ + @TableField(value = "run_command") + private String runCommand; + + /** + * 运行参数 + */ + @TableField(value = "run_params", typeHandler = FastjsonTypeHandler.class) + private JSONObject runParams; + + /** + * 算法用途 + */ + @TableField(value = "algorithm_usage") + private String algorithmUsage; + + /** + * 算法精度 + */ + @TableField(value = "accuracy") + private String accuracy; + + /** + * P4推理速度(ms) + */ + @TableField(value = "p4_inference_speed") + private Integer p4InferenceSpeed; + + /** + * 算法是否支持推理(1可推理,0不可推理) + */ + @TableField(value = "inference") + private Boolean inference; + + /** + * 训练结果输出(1是,0否) + */ + @TableField(value = "is_train_model_out") + private Boolean isTrainModelOut; + + /** + * 训练输出(1是,0否) + */ + @TableField(value = "is_train_out") + private Boolean isTrainOut; + + /** + * 可视化日志(1是,0否) + */ + @TableField(value = "is_visualized_log") + private Boolean isVisualizedLog; + + /** + * 算法状态 + */ + @TableField(value = "algorithm_status") + private Integer algorithmStatus; + + /** + * 资源拥有者ID + */ + @TableField(value = "origin_user_id",fill = FieldFill.INSERT) + private Long originUserId; +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithmUsage.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithmUsage.java new file mode 100644 index 0000000..8b9449d --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/entity/PtTrainAlgorithmUsage.java @@ -0,0 +1,68 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.dubhe.biz.db.entity.BaseEntity; + +import javax.validation.constraints.NotNull; + +/** + * @description 算法 + * @date 2020-06-23 + */ + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("pt_auxiliary_info") +@Accessors(chain = true) +public class PtTrainAlgorithmUsage extends BaseEntity { + + /** + * 主键 + */ + @TableId(value = "id", type = IdType.AUTO) + @NotNull(groups = {Update.class}) + private Long id; + + /** + * 用户id + */ + @TableField(value = "origin_user_id",fill = FieldFill.INSERT) + private Long originUserId; + + /** + * 类型 + */ + @TableField(value = "type") + private String type; + + /** + * 算法来源(1为我的算法,2为预置算法) + */ + @TableField(value = "aux_info") + private String auxInfo; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmQueryVO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmQueryVO.java new file mode 100644 index 0000000..4593b62 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmQueryVO.java @@ -0,0 +1,101 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.vo; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 训练算法返回列表 + * @date 2020-04-27 + */ +@Data +public class PtTrainAlgorithmQueryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "算法ID") + private Long id; + + @ApiModelProperty(value = "算法名称") + private String algorithmName; + + @ApiModelProperty(value = "描述信息") + private String description; + + @ApiModelProperty(value = "算法来源") + private Integer algorithmSource; + + @ApiModelProperty(value = "镜像名称") + private String imageName; + + @ApiModelProperty(value = "算法文件大小") + private String algorithmFileSize; + + @ApiModelProperty(value = "镜像版本") + private String imageTag; + + @ApiModelProperty(value = "代码目录") + private String codeDir; + + @ApiModelProperty(value = "运行命令") + private String runCommand; + + @ApiModelProperty(value = "运行参数") + private JSONObject runParams; + + @ApiModelProperty(value = "算法用途") + private String algorithmUsage; + + @ApiModelProperty(value = "精度") + private String accuracy; + + @ApiModelProperty(value = "P4推理速度") + private Integer p4InferenceSpeed; + + @ApiModelProperty(value = "输出结果(1是,0否)") + private Boolean isTrainModelOut; + + @ApiModelProperty(value = "输出信息(1是,0否)") + private Boolean isTrainOut; + + @ApiModelProperty(value = "可视化日志(1是,0否)") + private Boolean isVisualizedLog; + + @ApiModelProperty(value = "算法是否支持推理(1可推理,0不可推理)") + private Boolean inference; + + @ApiModelProperty(value = "创建人") + private Long createUserId; + + @ApiModelProperty(value = "创建时间") + private Timestamp createTime; + + @ApiModelProperty(value = "更新人") + private Long updateUserId; + + @ApiModelProperty(value = "更新时间") + private Timestamp updateTime; + + @ApiModelProperty(value = "资源拥有者ID") + private Long originUserId; +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmUsageQueryVO.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmUsageQueryVO.java new file mode 100644 index 0000000..52c84c6 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/domain/vo/PtTrainAlgorithmUsageQueryVO.java @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 算法用途返回列表 + * @date 2020-06-23 + */ +@Data +public class PtTrainAlgorithmUsageQueryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + private Long id; + + @ApiModelProperty(value = "类型") + private String type; + + @ApiModelProperty(value = "辅助信息") + private String auxInfo; + + @ApiModelProperty(value = "创建人") + private Long createUserId; + + @ApiModelProperty(value = "创建时间") + private Timestamp createTime; + + @ApiModelProperty(value = "更新人") + private Long updateUserId; + + @ApiModelProperty(value = "更新时间") + private Timestamp updateTime; + + @ApiModelProperty(value = "资源拥有者ID") + private Long originUserId; + + @ApiModelProperty(value = "是否为默认值") + private Boolean isDefault; + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmSourceEnum.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmSourceEnum.java new file mode 100644 index 0000000..2df6ac7 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmSourceEnum.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.enums; + +import lombok.Getter; + +/** + * @description 算法枚举类 + * @date 2020-05-12 + */ +@Getter +public enum AlgorithmSourceEnum { + + /** + * MINE 算法来源 我的算法 + */ + MINE(1, "MINE"), + /** + * PRE 算法来源 预置算法 + */ + PRE(2,"PRE"); + + private Integer status; + + private String message; + + AlgorithmSourceEnum(Integer status, String message) { + this.status = status; + this.message = message; + } +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmStatusEnum.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmStatusEnum.java new file mode 100644 index 0000000..dfdd78e --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/enums/AlgorithmStatusEnum.java @@ -0,0 +1,63 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.enums; + +/** + * @description 算法状态枚举 + * @date 2020-08-19 + */ +public enum AlgorithmStatusEnum { + + + /** + * 创建中 + */ + MAKING(0, "创建中"), + /** + * 创建成功 + */ + SUCCESS(1, "创建成功"), + /** + * 创建失败 + */ + FAIL(2, "创建失败"); + + + /** + * 编码 + */ + private Integer code; + + /** + * 描述 + */ + private String description; + + AlgorithmStatusEnum(int code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } +} + diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmController.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmController.java new file mode 100644 index 0000000..72ed003 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmController.java @@ -0,0 +1,124 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmQueryDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUpdateDTO; +import org.dubhe.algorithm.service.PtTrainAlgorithmService; +import org.dubhe.biz.base.annotation.ApiVersion; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.dto.ModelOptAlgorithmCreateDTO; +import org.dubhe.biz.base.dto.TrainAlgorithmSelectAllBatchIdDTO; +import org.dubhe.biz.base.dto.TrainAlgorithmSelectAllByIdDTO; +import org.dubhe.biz.base.dto.TrainAlgorithmSelectByIdDTO; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.base.vo.TrainAlgorithmQureyVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @description 训练算法 + * @date 2020-04-27 + */ +@Api(tags = "训练:算法管理") +@RestController +@RequestMapping("/algorithms") +public class PtTrainAlgorithmController { + + @Autowired + private PtTrainAlgorithmService ptTrainAlgorithmService; + + @GetMapping + @ApiOperation("查询算法") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM) + public DataResponseBody getAlgorithms(@Validated PtTrainAlgorithmQueryDTO ptTrainAlgorithmQueryDTO) { + return new DataResponseBody(ptTrainAlgorithmService.queryAll(ptTrainAlgorithmQueryDTO)); + } + + @GetMapping("/myAlgorithmCount") + @ApiOperation("查询当前用户的算法个数") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM) + public DataResponseBody getAlgorithmCount() { + return new DataResponseBody(ptTrainAlgorithmService.getAlgorithmCount()); + } + + @PostMapping + @ApiOperation("新增算法") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM_CREATE) + public DataResponseBody create(@Validated @RequestBody PtTrainAlgorithmCreateDTO ptTrainAlgorithmCreateDTO) { + return new DataResponseBody(ptTrainAlgorithmService.create(ptTrainAlgorithmCreateDTO)); + } + + @PutMapping + @ApiOperation("修改算法") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM_EDIT) + public DataResponseBody update(@Validated @RequestBody PtTrainAlgorithmUpdateDTO ptTrainAlgorithmUpdateDTO) { + return new DataResponseBody(ptTrainAlgorithmService.update(ptTrainAlgorithmUpdateDTO)); + } + + @DeleteMapping + @ApiOperation("删除算法") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM_DELETE) + public DataResponseBody deleteAll(@Validated @RequestBody PtTrainAlgorithmDeleteDTO ptTrainAlgorithmDeleteDTO) { + ptTrainAlgorithmService.deleteAll(ptTrainAlgorithmDeleteDTO); + return new DataResponseBody(); + } + + @GetMapping("/selectAllById") + @ApiOperation("根据Id查询所有数据") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM) + public DataResponseBody selectAllById(@Validated TrainAlgorithmSelectAllByIdDTO trainAlgorithmSelectAllByIdDTO) { + return new DataResponseBody(ptTrainAlgorithmService.selectAllById(trainAlgorithmSelectAllByIdDTO)); + } + + @GetMapping("/selectById") + @ApiOperation("根据Id查询") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM) + public DataResponseBody selectById(@Validated TrainAlgorithmSelectByIdDTO trainAlgorithmSelectByIdDTO) { + return new DataResponseBody(ptTrainAlgorithmService.selectById(trainAlgorithmSelectByIdDTO)); + } + + @GetMapping("/selectAllBatchIds") + @ApiOperation("批量查询") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM) + public DataResponseBody> selectAllBatchIds(@Validated TrainAlgorithmSelectAllBatchIdDTO trainAlgorithmSelectAllBatchIdDTO) { + return new DataResponseBody(ptTrainAlgorithmService.selectAllBatchIds(trainAlgorithmSelectAllBatchIdDTO)); + } + + @PostMapping("/uploadAlgorithm") + @ApiOperation("模型优化上传算法") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM_CREATE) + public DataResponseBody modelOptimizationUploadAlgorithm(@Validated @RequestBody ModelOptAlgorithmCreateDTO modelOptAlgorithmCreateDTO) { + return new DataResponseBody(ptTrainAlgorithmService.modelOptimizationUploadAlgorithm(modelOptAlgorithmCreateDTO)); + } + + @GetMapping("/getInferenceAlgorithm") + @ApiOperation("查询可推理算法") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM) + public DataResponseBody getInferenceAlgorithm() { + return new DataResponseBody(ptTrainAlgorithmService.getInferenceAlgorithm()); + } +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmUsageController.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmUsageController.java new file mode 100644 index 0000000..daede87 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/rest/PtTrainAlgorithmUsageController.java @@ -0,0 +1,84 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.algorithm.constant.UserAuxiliaryInfoConstant; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageQueryDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageUpdateDTO; +import org.dubhe.algorithm.service.PtTrainAlgorithmUsageService; +import org.dubhe.biz.base.annotation.ApiVersion; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.dataresponse.factory.DataResponseFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @description 算法用途管理 + * @date 2020-06-19 + */ +@Api(tags = "训练:算法用途管理") +@RestController +@RequestMapping("/algorithmUsage") +public class PtTrainAlgorithmUsageController { + + @Autowired + private PtTrainAlgorithmUsageService ptTrainAlgorithmUsageService; + + @GetMapping + @ApiOperation("算法用途列表展示") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM) + public DataResponseBody queryAll(@Validated PtTrainAlgorithmUsageQueryDTO ptTrainAlgorithmUsageQueryDTO) { + ptTrainAlgorithmUsageQueryDTO.setType(UserAuxiliaryInfoConstant.ALGORITHM_USAGE); + return DataResponseFactory + .success(ptTrainAlgorithmUsageService.queryAll(ptTrainAlgorithmUsageQueryDTO)); + } + + @PostMapping + @ApiOperation("新增算法用途") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM_CREATE) + public DataResponseBody create( + @Validated @RequestBody PtTrainAlgorithmUsageCreateDTO ptTrainAlgorithmUsageCreateDTO) { + ptTrainAlgorithmUsageCreateDTO.setType(UserAuxiliaryInfoConstant.ALGORITHM_USAGE); + return DataResponseFactory.success(ptTrainAlgorithmUsageService.create(ptTrainAlgorithmUsageCreateDTO)); + } + + @DeleteMapping + @ApiOperation("删除算法用途") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM_DELETE) + public DataResponseBody deleteAll(@Validated @RequestBody PtTrainAlgorithmUsageDeleteDTO ptTrainAlgorithmUsageDeleteDTO) { + ptTrainAlgorithmUsageService.deleteAll(ptTrainAlgorithmUsageDeleteDTO); + return new DataResponseBody(); + } + + @PutMapping + @ApiOperation("修改算法用途") + @PreAuthorize(Permissions.DEVELOPMENT_ALGORITHM_EDIT) + public DataResponseBody update( + @Validated @RequestBody PtTrainAlgorithmUsageUpdateDTO ptTrainAlgorithmUsageUpdateDTO) { + ptTrainAlgorithmUsageService.update(ptTrainAlgorithmUsageUpdateDTO); + return new DataResponseBody(); + } + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmService.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmService.java new file mode 100644 index 0000000..95bb9c7 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmService.java @@ -0,0 +1,121 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.service; + +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmQueryDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUpdateDTO; +import org.dubhe.algorithm.domain.vo.PtTrainAlgorithmQueryVO; +import org.dubhe.biz.base.dto.ModelOptAlgorithmCreateDTO; +import org.dubhe.biz.base.dto.TrainAlgorithmSelectAllBatchIdDTO; +import org.dubhe.biz.base.dto.TrainAlgorithmSelectAllByIdDTO; +import org.dubhe.biz.base.dto.TrainAlgorithmSelectByIdDTO; +import org.dubhe.biz.base.vo.ModelOptAlgorithmQureyVO; +import org.dubhe.biz.base.vo.TrainAlgorithmQureyVO; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; + +import java.util.List; +import java.util.Map; + +/** + * @description 训练算法 服务类 + * @date 2020-04-27 + */ +public interface PtTrainAlgorithmService { + + /** + * 查询数据分页 + * + * @param ptTrainAlgorithmQueryDTO 分页参数条件 + * @return Map map + */ + Map queryAll(PtTrainAlgorithmQueryDTO ptTrainAlgorithmQueryDTO); + + /** + * 新增算法 + * + * @param ptTrainAlgorithmCreateDTO 新增算法条件 + * @return PtTrainAlgorithmCreateVO 新建训练算法 + */ + List create(PtTrainAlgorithmCreateDTO ptTrainAlgorithmCreateDTO); + + /** + * 修改算法 + * + * @param ptTrainAlgorithmUpdateDTO 修改算法条件 + * @return PtTrainAlgorithmUpdateVO 修改训练算法 + */ + List update(PtTrainAlgorithmUpdateDTO ptTrainAlgorithmUpdateDTO); + + /** + * 删除算法 + * + * @param ptTrainAlgorithmDeleteDTO 删除算法条件 + */ + void deleteAll(PtTrainAlgorithmDeleteDTO ptTrainAlgorithmDeleteDTO); + + /** + * 查询当前用户的算法个数 + */ + Map getAlgorithmCount(); + + /** + * 根据Id查询所有数据(包含已被软删除的数据) + * + * @param trainAlgorithmSelectAllByIdDTO 算法id + * @return PtTrainAlgorithm 根据Id查询所有数据 + */ + TrainAlgorithmQureyVO selectAllById(TrainAlgorithmSelectAllByIdDTO trainAlgorithmSelectAllByIdDTO); + + /** + * 根据Id查询 + * + * @param trainAlgorithmSelectByIdDTO 算法id + * @return PtTrainAlgorithm 根据Id查询 + */ + TrainAlgorithmQureyVO selectById(TrainAlgorithmSelectByIdDTO trainAlgorithmSelectByIdDTO); + + /** + * 批量查询 + * + * @param trainAlgorithmSelectAllBatchIdDTO 算法ids + * @return List 批量查询 + */ + List selectAllBatchIds(TrainAlgorithmSelectAllBatchIdDTO trainAlgorithmSelectAllBatchIdDTO); + + /** + * 模型优化上传算法 + * + * @param modelOptAlgorithmCreateDTO 模型优化上传算法入参 + * @return ModelOptAlgorithmQureyVO 新增算法信息 + */ + ModelOptAlgorithmQureyVO modelOptimizationUploadAlgorithm(ModelOptAlgorithmCreateDTO modelOptAlgorithmCreateDTO); + + /** + * 算法删除文件还原 + * @param dto 还原实体 + */ + void algorithmRecycleFileRollback(RecycleCreateDTO dto); + + /** + * 查询可推理算法 + * @return List 返回可推理算法集合 + */ + List getInferenceAlgorithm(); +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmUsageService.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmUsageService.java new file mode 100644 index 0000000..ea48a91 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/PtTrainAlgorithmUsageService.java @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.service; + +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageQueryDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageUpdateDTO; + +import java.util.List; +import java.util.Map; + +/** + * @description 算法用途 服务类 + * @date 2020-06-23 + */ +public interface PtTrainAlgorithmUsageService { + + /** + * 查询算法用途 + * + * @param ptTrainAlgorithmUsageQueryDTO 查询算法用途参数 + */ + Map queryAll(PtTrainAlgorithmUsageQueryDTO ptTrainAlgorithmUsageQueryDTO); + + /** + * 新增算法用途 + * + * @param ptTrainAlgorithmUsageCreateDTO 新增算法用途参数 + */ + List create(PtTrainAlgorithmUsageCreateDTO ptTrainAlgorithmUsageCreateDTO); + + /** + * 删除算法用途 + * + * @param ptTrainAlgorithmUsageDeleteDTO 删除算法用途参数 + */ + void deleteAll(PtTrainAlgorithmUsageDeleteDTO ptTrainAlgorithmUsageDeleteDTO); + + /** + * 更新算法用途 + * + * @param ptTrainAlgorithmUsageUpdateDTO 更新算法用途参数 + */ + void update(PtTrainAlgorithmUsageUpdateDTO ptTrainAlgorithmUsageUpdateDTO); + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/RecycleFileService/AlgorithmRecycleFileServiceImpl.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/RecycleFileService/AlgorithmRecycleFileServiceImpl.java new file mode 100644 index 0000000..4d14809 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/RecycleFileService/AlgorithmRecycleFileServiceImpl.java @@ -0,0 +1,63 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.algorithm.service.RecycleFileService; + +import org.dubhe.algorithm.service.PtTrainAlgorithmService; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.dto.RecycleDetailCreateDTO; +import org.dubhe.recycle.global.AbstractGlobalRecycle; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * @description 算法文件自定义还原 + * @date 2021-03-22 + */ +@RefreshScope +@Component(value = "algorithmRecycleFile") +public class AlgorithmRecycleFileServiceImpl extends AbstractGlobalRecycle { + + /** + * 算法 service + */ + @Resource + private PtTrainAlgorithmService ptTrainAlgorithmService; + + /** + * 此方法不用,算法文件使用回收默认方法 + * + * @param detail 数据清理详情参数 + * @param dto 资源回收创建对象 + * @return + * @throws Exception + */ + @Override + protected boolean clearDetail(RecycleDetailCreateDTO detail, RecycleCreateDTO dto) throws Exception { + return false; + } + + /** + * 算法文件自定义还原方法 + * @param dto 还原实体 + */ + @Override + protected void rollback(RecycleCreateDTO dto) { + ptTrainAlgorithmService.algorithmRecycleFileRollback(dto); + } +} \ No newline at end of file diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmServiceImpl.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmServiceImpl.java new file mode 100644 index 0000000..89a9af1 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmServiceImpl.java @@ -0,0 +1,640 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.service.impl; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.map.HashedMap; +import org.dubhe.algorithm.async.TrainAlgorithmUploadAsync; +import org.dubhe.algorithm.client.ImageClient; +import org.dubhe.algorithm.client.NoteBookClient; +import org.dubhe.algorithm.constant.AlgorithmConstant; +import org.dubhe.algorithm.constant.TrainAlgorithmConfig; +import org.dubhe.algorithm.dao.PtTrainAlgorithmMapper; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmQueryDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUpdateDTO; +import org.dubhe.algorithm.domain.entity.PtTrainAlgorithm; +import org.dubhe.algorithm.domain.vo.PtTrainAlgorithmQueryVO; +import org.dubhe.algorithm.service.PtTrainAlgorithmService; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.*; +import org.dubhe.biz.base.enums.AlgorithmSourceEnum; +import org.dubhe.biz.base.enums.DatasetTypeEnum; +import org.dubhe.biz.base.enums.ImageTypeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.base.utils.ReflectionUtils; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.biz.base.vo.ModelOptAlgorithmQureyVO; +import org.dubhe.biz.base.vo.TrainAlgorithmQureyVO; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.file.api.FileStoreApi; +import org.dubhe.biz.file.enums.BizPathEnum; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.permission.annotation.DataPermissionMethod; +import org.dubhe.biz.permission.base.BaseService; +import org.dubhe.k8s.utils.K8sNameTool; +import org.dubhe.recycle.config.RecycleConfig; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.dto.RecycleDetailCreateDTO; +import org.dubhe.recycle.enums.RecycleModuleEnum; +import org.dubhe.recycle.enums.RecycleResourceEnum; +import org.dubhe.recycle.enums.RecycleTypeEnum; +import org.dubhe.recycle.service.RecycleService; +import org.dubhe.recycle.utils.RecycleTool; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + + +/** + * @description 训练算法 服务实现类 + * @date 2020-04-27 + */ +@Service +public class PtTrainAlgorithmServiceImpl implements PtTrainAlgorithmService { + + @Autowired + private PtTrainAlgorithmMapper ptTrainAlgorithmMapper; + + @Autowired + private ImageClient imageClient; + + @Autowired + private K8sNameTool k8sNameTool; + + @Autowired + private TrainAlgorithmConfig trainAlgorithmConstant; + + @Autowired + private NoteBookClient noteBookClient; + + @Autowired + private TrainAlgorithmUploadAsync algorithmUpdateAsync; + + @Autowired + private RecycleService recycleService; + + @Autowired + private RecycleConfig recycleConfig; + + @Autowired + private UserContextService userContext; + + @Resource(name = "hostFileStoreApiImpl") + private FileStoreApi fileStoreApi; + + public final static List FIELD_NAMES; + + static { + FIELD_NAMES = ReflectionUtils.getFieldNames(PtTrainAlgorithmQueryVO.class); + } + + /** + * 查询数据分页 + * + * @param ptTrainAlgorithmQueryDTO 条件 + * @return Map 返回查询数据 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public Map queryAll(PtTrainAlgorithmQueryDTO ptTrainAlgorithmQueryDTO) { + //获取用户信息 + UserContext user = userContext.getCurUser(); + //当算法来源为空时,设置默认算法来源 + if (ptTrainAlgorithmQueryDTO.getAlgorithmSource() == null) { + ptTrainAlgorithmQueryDTO.setAlgorithmSource(trainAlgorithmConstant.getAlgorithmSource()); + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("algorithm_source", ptTrainAlgorithmQueryDTO.getAlgorithmSource()); + //判断算法来源 + if (AlgorithmSourceEnum.MINE.getStatus().equals(ptTrainAlgorithmQueryDTO.getAlgorithmSource())) { + if (!BaseService.isAdmin(user)) { + wrapper.eq("create_user_id", userContext.getCurUserId()); + } + } + //根据算法用途筛选 + if (ptTrainAlgorithmQueryDTO.getAlgorithmUsage() != null) { + wrapper.like("algorithm_usage", ptTrainAlgorithmQueryDTO.getAlgorithmUsage()); + } + //根据算法是否可推理筛选 + if (ptTrainAlgorithmQueryDTO.getInference() != null) { + wrapper.eq("inference", ptTrainAlgorithmQueryDTO.getInference()); + } + if (!StringUtils.isEmpty(ptTrainAlgorithmQueryDTO.getAlgorithmName())) { + wrapper.and(qw -> qw.eq("id", ptTrainAlgorithmQueryDTO.getAlgorithmName()).or().like("algorithm_name", + ptTrainAlgorithmQueryDTO.getAlgorithmName())); + } + + Page page = ptTrainAlgorithmQueryDTO.toPage(); + IPage ptTrainAlgorithms; + try { + if (ptTrainAlgorithmQueryDTO.getSort() != null && FIELD_NAMES.contains(ptTrainAlgorithmQueryDTO.getSort())) { + if (AlgorithmConstant.SORT_ASC.equalsIgnoreCase(ptTrainAlgorithmQueryDTO.getOrder())) { + wrapper.orderByAsc(StringUtils.humpToLine(ptTrainAlgorithmQueryDTO.getSort())); + } else { + wrapper.orderByDesc(StringUtils.humpToLine(ptTrainAlgorithmQueryDTO.getSort())); + } + } else { + wrapper.orderByDesc(AlgorithmConstant.ID); + } + ptTrainAlgorithms = ptTrainAlgorithmMapper.selectPage(page, wrapper); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "Query training algorithm list display exceptions :{}, request information :{}", e, + ptTrainAlgorithmQueryDTO); + throw new BusinessException("查询训练算法列表展示异常"); + } + List ptTrainAlgorithmQueryResult = ptTrainAlgorithms.getRecords().stream().map(x -> { + PtTrainAlgorithmQueryVO ptTrainAlgorithmQueryVO = new PtTrainAlgorithmQueryVO(); + BeanUtils.copyProperties(x, ptTrainAlgorithmQueryVO); + //获取镜像名称与版本 + getImageNameAndImageTag(x, ptTrainAlgorithmQueryVO); + return ptTrainAlgorithmQueryVO; + }).collect(Collectors.toList()); + return PageUtil.toPage(page, ptTrainAlgorithmQueryResult); + } + + /** + * 新增算法 + * + * @param ptTrainAlgorithmCreateDTO 新增算法条件 + * @return idList 返回新增算法 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public List create(PtTrainAlgorithmCreateDTO ptTrainAlgorithmCreateDTO) { + //获取用户信息 + UserContext user = userContext.getCurUser(); + //获取镜像url + BaseImageDTO baseImageDTO = new BaseImageDTO(); + BeanUtils.copyProperties(ptTrainAlgorithmCreateDTO, baseImageDTO); + if (StringUtils.isNotBlank(ptTrainAlgorithmCreateDTO.getImageName()) && StringUtils.isNotBlank(ptTrainAlgorithmCreateDTO.getImageTag())) { + ptTrainAlgorithmCreateDTO.setImageName(getImageUrl(baseImageDTO, user)); + } + //创建算法校验DTO并设置默认值 + setAlgorithmDtoDefault(ptTrainAlgorithmCreateDTO); + //算法路径 + String path = fileStoreApi.getBucket() + ptTrainAlgorithmCreateDTO.getCodeDir(); + if (!fileStoreApi.fileOrDirIsExist(fileStoreApi.getRootDir() + path)) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "The user {} upload path {} does not exist", user.getUsername(), path); + throw new BusinessException("算法文件或路径不存在"); + } + //保存算法 + PtTrainAlgorithm ptTrainAlgorithm = new PtTrainAlgorithm(); + BeanUtils.copyProperties(ptTrainAlgorithmCreateDTO, ptTrainAlgorithm); + //创建我的算法 + if (BaseService.isAdmin(user) && AlgorithmSourceEnum.PRE.getStatus().equals(ptTrainAlgorithmCreateDTO.getAlgorithmSource())) { + ptTrainAlgorithm.setAlgorithmSource(AlgorithmSourceEnum.PRE.getStatus()); + ptTrainAlgorithm.setOriginUserId(0L); + } else { + ptTrainAlgorithm.setAlgorithmSource(AlgorithmSourceEnum.MINE.getStatus()); + } + ptTrainAlgorithm.setCreateUserId(user.getId()); + + //算法名称校验 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("algorithm_name", ptTrainAlgorithmCreateDTO.getAlgorithmName()).and(wrapper -> wrapper.eq("create_user_id", user.getId()).or().eq("origin_user_id", 0L)); + Integer countResult = ptTrainAlgorithmMapper.selectCount(queryWrapper); + //如果是通过【保存至算法】接口创建算法,名称重复可用随机数生成新算法名,待后续客户自主修改 + if (countResult > 0) { + if (ptTrainAlgorithmCreateDTO.getNoteBookId() != null) { + String randomStr = RandomUtil.randomNumbers(MagicNumConstant.FOUR); + ptTrainAlgorithm.setAlgorithmName(ptTrainAlgorithmCreateDTO.getAlgorithmName() + randomStr); + } else { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "The algorithm name ({}) already exists", ptTrainAlgorithmCreateDTO.getAlgorithmName()); + throw new BusinessException("算法名称已存在,请重新输入"); + } + } + //校验path是否带有压缩文件,如有,则解压至算法文件夹下并删除压缩文件 + if (path.toLowerCase().endsWith(AlgorithmConstant.COMPRESS_ZIP)) { + unZip(user, path, ptTrainAlgorithm, ptTrainAlgorithmCreateDTO); + } + + //校验上传算法是否支持推理,如有,则拷贝至算法文件夹下 + if (ptTrainAlgorithmCreateDTO.getInference()) { + //可推理的算法文件拷贝 + copyFile(user, path, ptTrainAlgorithm, ptTrainAlgorithmCreateDTO); + } + + try { + //算法未保存成功,抛出异常,并返回失败信息 + ptTrainAlgorithmMapper.insert(ptTrainAlgorithm); + //设置子线程共享 + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + RequestContextHolder.setRequestAttributes(servletRequestAttributes, true); + //上传算法异步处理 + algorithmUpdateAsync.createTrainAlgorithm(userContext.getCurUser(), ptTrainAlgorithm, ptTrainAlgorithmCreateDTO); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "The user {} saving algorithm was not successful. Failure reason :{}", user.getUsername(), e.getMessage()); + throw new BusinessException("算法未保存成功"); + } + return Collections.singletonList(ptTrainAlgorithm.getId()); + } + + /** + * 修改算法 + * + * @param ptTrainAlgorithmUpdateDTO 修改算法条件 + * @return PtTrainAlgorithmUpdateVO 返回修改算法 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public List update(PtTrainAlgorithmUpdateDTO ptTrainAlgorithmUpdateDTO) { + //获取用户信息 + UserContext user = userContext.getCurUser(); + //权限校验 + PtTrainAlgorithm ptTrainAlgorithm = ptTrainAlgorithmMapper.selectById(ptTrainAlgorithmUpdateDTO.getId()); + if (null == ptTrainAlgorithm) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "It is illegal for the user {} to modify the algorithm with id {}", user.getUsername(), ptTrainAlgorithmUpdateDTO.getId()); + throw new BusinessException("您修改的算法不存在或已被删除"); + } + PtTrainAlgorithm updatePtAlgorithm = new PtTrainAlgorithm(); + updatePtAlgorithm.setId(ptTrainAlgorithm.getId()).setUpdateUserId(user.getId()); + //判断是否修改算法名称 + if (StringUtils.isNotBlank(ptTrainAlgorithmUpdateDTO.getAlgorithmName())) { + //算法名称校验 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("algorithm_name", ptTrainAlgorithmUpdateDTO.getAlgorithmName()) + .ne("id", ptTrainAlgorithmUpdateDTO.getId()); + Integer countResult = ptTrainAlgorithmMapper.selectCount(queryWrapper); + if (countResult > 0) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "The algorithm name ({}) already exists", ptTrainAlgorithmUpdateDTO.getAlgorithmName()); + throw new BusinessException("算法名称已存在,请重新输入"); + } + updatePtAlgorithm.setAlgorithmName(ptTrainAlgorithmUpdateDTO.getAlgorithmName()); + } + //判断是否修改算法描述 + if (ptTrainAlgorithmUpdateDTO.getDescription() != null) { + updatePtAlgorithm.setDescription(ptTrainAlgorithmUpdateDTO.getDescription()); + } + //判断是否修改算法用途 + if (ptTrainAlgorithmUpdateDTO.getAlgorithmUsage() != null) { + updatePtAlgorithm.setAlgorithmUsage(ptTrainAlgorithmUpdateDTO.getAlgorithmUsage()); + } + //判断是否修改训练输出 + if (ptTrainAlgorithmUpdateDTO.getIsTrainOut() != null) { + updatePtAlgorithm.setIsTrainOut(ptTrainAlgorithmUpdateDTO.getIsTrainOut()); + } + //判断是否修改可视化日志 + if (ptTrainAlgorithmUpdateDTO.getIsVisualizedLog() != null) { + updatePtAlgorithm.setIsVisualizedLog(ptTrainAlgorithmUpdateDTO.getIsVisualizedLog()); + } + try { + //算法未修改成功,抛出异常,并返回失败信息 + ptTrainAlgorithmMapper.updateById(updatePtAlgorithm); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} failed to modify the algorithm. Pt_train_algorithm table modification operation failed. Failure reason :{}", user.getUsername(), e.getMessage()); + throw new BusinessException("修改失败"); + } + return Collections.singletonList(ptTrainAlgorithm.getId()); + } + + /** + * 可推理的算法文件拷贝 + * + * @param user 用户 + * @param path 文件路径 + * @param ptTrainAlgorithm 算法参数 + */ + private void copyFile(UserContext user, String path, PtTrainAlgorithm ptTrainAlgorithm, PtTrainAlgorithmCreateDTO ptTrainAlgorithmCreateDTO) { + //目标路径 + String targetPath = null; + if (BaseService.isAdmin(user) && AlgorithmSourceEnum.PRE.getStatus().equals(ptTrainAlgorithmCreateDTO.getAlgorithmSource())) { + targetPath = k8sNameTool.getPrePath(BizPathEnum.ALGORITHM, user.getId()); + } else { + targetPath = k8sNameTool.getPath(BizPathEnum.ALGORITHM, user.getId()); + } + boolean copyFile; + if (fileStoreApi.isDirectory(fileStoreApi.getRootDir() + path)) { + copyFile = fileStoreApi.copyPath(fileStoreApi.getRootDir() + path, fileStoreApi.getRootDir() + fileStoreApi.getBucket() + targetPath); + } else { + copyFile = fileStoreApi.copyFile(fileStoreApi.getRootDir() + path, fileStoreApi.getRootDir() + fileStoreApi.getBucket() + targetPath); + } + if (!copyFile) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} failed to inference copyFile", user.getUsername()); + throw new BusinessException("文件拷贝失败"); + } + //算法路径 + ptTrainAlgorithm.setCodeDir(targetPath); + //算法文件可推理 + ptTrainAlgorithm.setInference(true); + } + + /** + * 解压缩zip压缩包 + * + * @param user 用户 + * @param path 文件路径 + * @param ptTrainAlgorithm 算法参数 + */ + private void unZip(UserContext user, String path, PtTrainAlgorithm ptTrainAlgorithm, PtTrainAlgorithmCreateDTO ptTrainAlgorithmCreateDTO) { + //目标路径 + String targetPath = null; + if (BaseService.isAdmin(user) && AlgorithmSourceEnum.PRE.getStatus().equals(ptTrainAlgorithmCreateDTO.getAlgorithmSource())) { + targetPath = k8sNameTool.getPrePath(BizPathEnum.ALGORITHM, user.getId()); + } else { + targetPath = k8sNameTool.getPath(BizPathEnum.ALGORITHM, user.getId()); + } + boolean unzip = fileStoreApi.unzip(path, fileStoreApi.getBucket() + targetPath); + if (!unzip) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} failed to unzip", user.getUsername()); + throw new BusinessException("内部错误"); + } + //算法路径 + ptTrainAlgorithm.setCodeDir(targetPath); + } + + /** + * 删除算法 + * + * @param ptTrainAlgorithmDeleteDTO 删除算法条件 + */ + @Override + @Transactional(rollbackFor = Exception.class) + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public void deleteAll(PtTrainAlgorithmDeleteDTO ptTrainAlgorithmDeleteDTO) { + //获取用户信息 + UserContext user = userContext.getCurUser(); + Set idList = ptTrainAlgorithmDeleteDTO.getIds(); + //权限校验 + QueryWrapper query = new QueryWrapper<>(); + //非管理员不可删除预置算法 + if (!BaseService.isAdmin(user)) { + query.eq("algorithm_source", 1); + } + query.in("id", idList); + List algorithmList = ptTrainAlgorithmMapper.selectList(query); + if (algorithmList.size() < idList.size()) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} delete algorithm failed, no permission to delete the corresponding data in the algorithm table", user.getUsername()); + throw new BusinessException("您删除的ID不存在或已被删除"); + } + int deleteCountResult = ptTrainAlgorithmMapper.deleteBatchIds(idList); + if (deleteCountResult < idList.size()) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "The user {} deletion algorithm failed, and the algorithm table deletion operation based on the ID array {} failed", user.getUsername(), ptTrainAlgorithmDeleteDTO.getIds()); + throw new BusinessException("删除算法未成功"); + } + //同步更新noteBook表中algorithmId=0 + NoteBookAlgorithmQueryDTO noteBookAlgorithmQueryDTO = new NoteBookAlgorithmQueryDTO(); + List ids = new ArrayList<>(); + idList.stream().forEach(id -> { + ids.add(id); + }); + noteBookAlgorithmQueryDTO.setAlgorithmIdList(ids); + DataResponseBody> dataResponseBody = noteBookClient.getNoteBookIdByAlgorithm(noteBookAlgorithmQueryDTO); + if (dataResponseBody.succeed()) { + List noteBookIdList = dataResponseBody.getData(); + if (!CollectionUtils.isEmpty(noteBookIdList)) { + //根据算法 + NoteBookAlgorithmUpdateDTO noteBookAlgorithmUpdateDTO = new NoteBookAlgorithmUpdateDTO(); + noteBookAlgorithmUpdateDTO.setNotebookIdList(noteBookIdList); + noteBookAlgorithmUpdateDTO.setAlgorithmId(0L); + noteBookClient.updateNoteBookAlgorithm(noteBookAlgorithmUpdateDTO); + } + } + //定时任务删除相应的算法文件 + for (PtTrainAlgorithm algorithm : algorithmList) { + RecycleCreateDTO recycleCreateDTO = new RecycleCreateDTO(); + recycleCreateDTO.setRecycleModule(RecycleModuleEnum.BIZ_ALGORITHM.getValue()) + .setRecycleDelayDate(recycleConfig.getAlgorithmValid()) + .setRecycleNote(RecycleTool.generateRecycleNote("删除算法文件", algorithm.getAlgorithmName(), algorithm.getId())) + .setRemark(algorithm.getId().toString()) + .setRestoreCustom(RecycleResourceEnum.ALGORITHM_RECYCLE_FILE.getClassName()); + RecycleDetailCreateDTO detail = new RecycleDetailCreateDTO(); + detail.setRecycleType(RecycleTypeEnum.FILE.getCode()) + .setRecycleCondition(fileStoreApi.formatPath(fileStoreApi.getRootDir() + fileStoreApi.getBucket() + algorithm.getCodeDir())); + recycleCreateDTO.addRecycleDetailCreateDTO(detail); + recycleService.createRecycleTask(recycleCreateDTO); + } + } + + /** + * 查询算法个数 + * + * @return count 返回个数 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public Map getAlgorithmCount() { + QueryWrapper wrapper = new QueryWrapper(); + wrapper.eq("algorithm_source", AlgorithmSourceEnum.MINE.getStatus()); + Integer countResult = ptTrainAlgorithmMapper.selectCount(wrapper); + return new HashedMap() {{ + put("count", countResult); + }}; + } + + /** + * 根据Id查询所有数据(包含已被软删除的数据) + * @param trainAlgorithmSelectAllByIdDTO 算法id + * @return TrainAlgorithmQureyVO返回查询数据(包含已被软删除的数据) + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public TrainAlgorithmQureyVO selectAllById(TrainAlgorithmSelectAllByIdDTO trainAlgorithmSelectAllByIdDTO) { + PtTrainAlgorithm ptTrainAlgorithm = ptTrainAlgorithmMapper.selectAllById(trainAlgorithmSelectAllByIdDTO.getId()); + TrainAlgorithmQureyVO trainAlgorithmQureyVO = new TrainAlgorithmQureyVO(); + BeanUtils.copyProperties(ptTrainAlgorithm, trainAlgorithmQureyVO); + return trainAlgorithmQureyVO; + } + + /** + * 根据Id查询 + * @param trainAlgorithmSelectByIdDTO 算法id + * @return TrainAlgorithmQureyVO 返回查询数据 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public TrainAlgorithmQureyVO selectById(TrainAlgorithmSelectByIdDTO trainAlgorithmSelectByIdDTO) { + PtTrainAlgorithm ptTrainAlgorithm = ptTrainAlgorithmMapper.selectById(trainAlgorithmSelectByIdDTO.getId()); + TrainAlgorithmQureyVO trainAlgorithmQureyVO = new TrainAlgorithmQureyVO(); + BeanUtils.copyProperties(ptTrainAlgorithm, trainAlgorithmQureyVO); + return trainAlgorithmQureyVO; + } + + /** + * 根据Id批量查询 + * @param trainAlgorithmSelectAllBatchIdDTO 算法ids + * @return List 返回查询数据 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public List selectAllBatchIds(TrainAlgorithmSelectAllBatchIdDTO trainAlgorithmSelectAllBatchIdDTO) { + List ptTrainAlgorithms = ptTrainAlgorithmMapper.selectAllBatchIds(trainAlgorithmSelectAllBatchIdDTO.getIds()); + List trainAlgorithmQureyVOS = ptTrainAlgorithms.stream().map(x -> { + TrainAlgorithmQureyVO trainAlgorithmQureyVO = new TrainAlgorithmQureyVO(); + BeanUtils.copyProperties(x, trainAlgorithmQureyVO); + return trainAlgorithmQureyVO; + }).collect(Collectors.toList()); + return trainAlgorithmQureyVOS; + } + + /** + * 获取镜像名称与版本 + * + * @param trainAlgorithm 镜像URL + * @param ptTrainAlgorithmQueryVO 镜像名称与版本 + */ + private void getImageNameAndImageTag(PtTrainAlgorithm trainAlgorithm, PtTrainAlgorithmQueryVO ptTrainAlgorithmQueryVO) { + if (StringUtils.isNotBlank(trainAlgorithm.getImageName())) { + String imageNameSuffix = trainAlgorithm.getImageName().substring(trainAlgorithm.getImageName().lastIndexOf(StrUtil.SLASH) + MagicNumConstant.ONE); + String[] imageNameSuffixArray = imageNameSuffix.split(StrUtil.COLON); + ptTrainAlgorithmQueryVO.setImageName(imageNameSuffixArray[0]); + ptTrainAlgorithmQueryVO.setImageTag(imageNameSuffixArray[1]); + } + } + + + /** + * 创建算法DTO校验并设置默认值 + * + * @param dto 校验DTO + **/ + private void setAlgorithmDtoDefault(PtTrainAlgorithmCreateDTO dto) { + + //设置fork默认值(fork:创建算法来源) + if (dto.getFork() == null) { + dto.setFork(trainAlgorithmConstant.getFork()); + } + //设置inference默认值(inference:上传算法是否支持推理) + if (dto.getInference() == null) { + dto.setInference(trainAlgorithmConstant.getInference()); + } + //设置是否输出训练信息 + if (dto.getIsTrainOut() == null) { + dto.setIsTrainOut(trainAlgorithmConstant.getIsTrainOut()); + } + //设置是否输出训练结果 + if (dto.getIsTrainModelOut() == null) { + dto.setIsTrainModelOut(trainAlgorithmConstant.getIsTrainModelOut()); + } + //设置是否输出可视化日志 + if (dto.getIsVisualizedLog() == null) { + dto.setIsVisualizedLog(trainAlgorithmConstant.getIsVisualizedLog()); + } + } + + /** + * 获取镜像url + * + * @param baseImageDto 镜像参数 + * @return BaseImageDTO 镜像url + **/ + private String getImageUrl(BaseImageDTO baseImageDto, UserContext user) { + + PtImageQueryUrlDTO ptImageQueryUrlDTO = new PtImageQueryUrlDTO(); + ptImageQueryUrlDTO.setImageTag(baseImageDto.getImageTag()); + ptImageQueryUrlDTO.setImageName(baseImageDto.getImageName()); + ptImageQueryUrlDTO.setProjectType(ImageTypeEnum.TRAIN.getType()); + DataResponseBody dataResponseBody = imageClient.getImageUrl(ptImageQueryUrlDTO); + if (!dataResponseBody.succeed()) { + LogUtil.error(LogEnum.BIZ_TRAIN, " User {} gets image ,the imageName is {}, the imageTag is {}, and the result of dubhe-image service call failed", user.getUsername(), baseImageDto.getImageName(), baseImageDto.getImageTag()); + throw new BusinessException("镜像服务调用失败"); + } + String ptImage = dataResponseBody.getData(); + // 镜像路径 + if (StringUtils.isBlank(ptImage)) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} gets image ,the imageName is {}, the imageTag is {}, and the result of query image table (PT_image) is empty", user.getUsername(), baseImageDto.getImageName(), baseImageDto.getImageTag()); + throw new BusinessException("镜像不存在"); + } + return ptImage; + } + + /** + * + * @param modelOptAlgorithmCreateDTO 模型优化上传算法入参 + * @return PtTrainAlgorithm 新增算法信息 + */ + @Override + public ModelOptAlgorithmQureyVO modelOptimizationUploadAlgorithm(ModelOptAlgorithmCreateDTO modelOptAlgorithmCreateDTO) { + PtTrainAlgorithmCreateDTO ptTrainAlgorithmCreateDTO = new PtTrainAlgorithmCreateDTO(); + ptTrainAlgorithmCreateDTO.setAlgorithmName(modelOptAlgorithmCreateDTO.getName()).setCodeDir(modelOptAlgorithmCreateDTO.getPath()).setAlgorithmUsage("模型优化").setIsTrainModelOut(false).setIsTrainOut(false).setIsVisualizedLog(false); + List ids = create(ptTrainAlgorithmCreateDTO); + PtTrainAlgorithm ptTrainAlgorithm = ptTrainAlgorithmMapper.selectById(ids.get(NumberConstant.NUMBER_0)); + ModelOptAlgorithmQureyVO modelOptAlgorithmQureyVO = new ModelOptAlgorithmQureyVO(); + BeanUtils.copyProperties(ptTrainAlgorithm, modelOptAlgorithmQureyVO); + return modelOptAlgorithmQureyVO; + } + + /** + * 算法删除文件还原 + * @param dto 还原实体 + */ + @Override + public void algorithmRecycleFileRollback(RecycleCreateDTO dto) { + //获取用户信息 + UserContext user = userContext.getCurUser(); + if (dto == null) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} restore algorithm failed to delete the file because RecycleCreateDTO is null", user.getUsername()); + throw new BusinessException("非法入参"); + } + Long algorithmId = Long.valueOf(dto.getRemark()); + PtTrainAlgorithm ptTrainAlgorithm = ptTrainAlgorithmMapper.selectAllById(algorithmId); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("algorithm_name", ptTrainAlgorithm.getAlgorithmName()); + if (ptTrainAlgorithmMapper.selectCount(wrapper) > 0) { + throw new BusinessException("算法已存在"); + } + try { + ptTrainAlgorithmMapper.updateStatusById(algorithmId, false); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} restore algorithm failed to delete the file because:{}", user.getUsername(), e); + throw new BusinessException("还原失败"); + } + } + + /** + * 查询可推理算法 + * @return List 返回可推理算法集合 + */ + @Override + public List getInferenceAlgorithm() { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("inference", true).orderByDesc("id"); + List ptTrainAlgorithms = ptTrainAlgorithmMapper.selectList(wrapper); + if (CollectionUtils.isEmpty(ptTrainAlgorithms)) { + return null; + } + List ptTrainAlgorithmQueryResult = ptTrainAlgorithms.stream().map(x -> { + PtTrainAlgorithmQueryVO ptTrainAlgorithmQueryVO = new PtTrainAlgorithmQueryVO(); + BeanUtils.copyProperties(x, ptTrainAlgorithmQueryVO); + //获取镜像名称与版本 + getImageNameAndImageTag(x, ptTrainAlgorithmQueryVO); + return ptTrainAlgorithmQueryVO; + }).collect(Collectors.toList()); + return ptTrainAlgorithmQueryResult; + } + + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmUsageServiceImpl.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmUsageServiceImpl.java new file mode 100644 index 0000000..24e2e54 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/service/impl/PtTrainAlgorithmUsageServiceImpl.java @@ -0,0 +1,186 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dubhe.algorithm.dao.PtTrainAlgorithmUsageMapper; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageQueryDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageUpdateDTO; +import org.dubhe.algorithm.domain.entity.PtTrainAlgorithmUsage; +import org.dubhe.algorithm.domain.vo.PtTrainAlgorithmUsageQueryVO; +import org.dubhe.algorithm.service.PtTrainAlgorithmUsageService; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.ResponseCode; +import org.dubhe.biz.base.context.DataContext; +import org.dubhe.biz.base.context.UserContext; +import org.dubhe.biz.base.dto.CommonPermissionDataDTO; +import org.dubhe.biz.base.enums.DatasetTypeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.service.UserContextService; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.permission.annotation.DataPermissionMethod; +import org.dubhe.biz.permission.aspect.PermissionAspect; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @description 用途管理 服务实现类 + * @date 2020-06-23 + */ +@Service +public class PtTrainAlgorithmUsageServiceImpl implements PtTrainAlgorithmUsageService { + + @Autowired + private PtTrainAlgorithmUsageMapper ptTrainAlgorithUsagemMapper; + + @Autowired + private UserContextService userContextService; + + /** + * 算法用途 + * + * @param ptTrainAlgorithmUsageQueryDTO 查询算法用途参数 + * @return Map 返回查询算法用途分页 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public Map queryAll(PtTrainAlgorithmUsageQueryDTO ptTrainAlgorithmUsageQueryDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + QueryWrapper wrapper = WrapperHelp.getWrapper(ptTrainAlgorithmUsageQueryDTO); + + Page page = ptTrainAlgorithmUsageQueryDTO.toPage(); + IPage ptTrainAlgorithms = null; + + if (ptTrainAlgorithmUsageQueryDTO.getIsContainDefault()) { + wrapper.in("origin_user_id", user.getId(), PermissionAspect.PUBLIC_DATA_USER_ID); + } else { + wrapper.eq("origin_user_id", user.getId()); + } + + wrapper.eq("type", ptTrainAlgorithmUsageQueryDTO.getType()); + + DataContext.set(CommonPermissionDataDTO.builder().type(true).build()); + ptTrainAlgorithms = ptTrainAlgorithUsagemMapper.selectPage(page, wrapper); + DataContext.remove(); + + List ptTrainAlgorithmUsageQueryResult = ptTrainAlgorithms.getRecords().stream() + .map(x -> { + PtTrainAlgorithmUsageQueryVO ptTrainAlgorithmUsageQueryVO = new PtTrainAlgorithmUsageQueryVO(); + BeanUtils.copyProperties(x, ptTrainAlgorithmUsageQueryVO); + ptTrainAlgorithmUsageQueryVO.setIsDefault(Objects.equals(x.getOriginUserId(), PermissionAspect.PUBLIC_DATA_USER_ID)); + return ptTrainAlgorithmUsageQueryVO; + }).collect(Collectors.toList()); + return PageUtil.toPage(page, ptTrainAlgorithmUsageQueryResult); + } + + /** + * 新增算法用途 + * + * @param ptTrainAlgorithmUsageCreateDTO 新增算法用途参数 + * @return List 返回新增算法用途 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public List create(PtTrainAlgorithmUsageCreateDTO ptTrainAlgorithmUsageCreateDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + PtTrainAlgorithmUsage ptTrainAlgorithmUsage = new PtTrainAlgorithmUsage(); + ptTrainAlgorithmUsage.setAuxInfo(ptTrainAlgorithmUsageCreateDTO.getAuxInfo()) + .setType(ptTrainAlgorithmUsageCreateDTO.getType()); + + int insertResult = ptTrainAlgorithUsagemMapper.insert(ptTrainAlgorithmUsage); + + if (insertResult < 1) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} secondary information was not saved successfully", user.getUsername()); + throw new BusinessException("用户辅助信息未保存成功"); + } + return Collections.singletonList(ptTrainAlgorithmUsage.getId()); + } + + /** + * 删除算法用途 + * + * @param ptTrainAlgorithmUsageDeleteDTO 删除算法用途参数 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAll(PtTrainAlgorithmUsageDeleteDTO ptTrainAlgorithmUsageDeleteDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + Set idList = Stream.of(ptTrainAlgorithmUsageDeleteDTO.getIds()).collect(Collectors.toSet()); + QueryWrapper query = new QueryWrapper<>(); + query.in("id", idList); + Integer queryCountResult = ptTrainAlgorithUsagemMapper.selectCount(query); + + if (queryCountResult < idList.size()) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} failed to delete user secondary. No permissions to delete corresponding data in user secondary table", user.getUsername()); + throw new BusinessException("您删除的ID不存在或已被删除"); + } + int deleteCountResult = ptTrainAlgorithUsagemMapper.deleteBatchIds(idList); + + if (deleteCountResult < idList.size()) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} failed to delete user assistance information. User service deletion based on id array {} failed", user.getUsername(), ptTrainAlgorithmUsageDeleteDTO.getIds()); + throw new BusinessException("内部错误"); + } + } + + /** + *更新算法用途 + * + * @param ptTrainAlgorithmUsageUpdateDTO 更新算法用途参数 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(PtTrainAlgorithmUsageUpdateDTO ptTrainAlgorithmUsageUpdateDTO) { + //获取用户信息 + UserContext user = userContextService.getCurUser(); + QueryWrapper query = new QueryWrapper<>(); + query.in("id", ptTrainAlgorithmUsageUpdateDTO.getId()); + Integer queryIntResult = ptTrainAlgorithUsagemMapper.selectCount(query); + + if (queryIntResult < MagicNumConstant.ONE) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} failed to modify user auxiliary information and has no permission to modify the corresponding data in the algorithm table", user.getUsername()); + throw new BusinessException("您修改的ID不存在或已被删除,请重新输入"); + } + PtTrainAlgorithmUsage ptTrainAlgorithmUsage = new PtTrainAlgorithmUsage(); + ptTrainAlgorithmUsage.setId(ptTrainAlgorithmUsageUpdateDTO.getId()); + ptTrainAlgorithmUsage.setAuxInfo(ptTrainAlgorithmUsageUpdateDTO.getAuxInfo()); + + int updateResult = ptTrainAlgorithUsagemMapper.updateById(ptTrainAlgorithmUsage); + if (updateResult < 1) { + LogUtil.error(LogEnum.BIZ_ALGORITHM, "User {} failed to modify user assistance information", user.getUsername()); + throw new BusinessException("内部错误"); + } + + } + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/utils/TrainUtil.java b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/utils/TrainUtil.java new file mode 100644 index 0000000..3e4e55d --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/java/org/dubhe/algorithm/utils/TrainUtil.java @@ -0,0 +1,80 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm.utils; + +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.SymbolConstant; + +import java.sql.Timestamp; + +/** + * @description 训练任务工具类 + * @date 2020-07-14 + */ +public class TrainUtil { + + + private TrainUtil() { + + } + + public static final String REGEXP = "^[a-zA-Z0-9\\-\\_\\u4e00-\\u9fa5]+$"; + public static final String REGEXP_NAME = "^[a-zA-Z0-9\\-\\_]+$"; + public static final String REGEXP_TAG = "^[a-zA-Z0-9\\-\\_\\.]+$"; + + public static final String RUNTIME = "%02d:%02d:%02d"; + public static final String FOUR_DECIMAL = "%04d"; + public static final String FOUR_TWO = "%.2f"; + + public static final int NUMBER_ZERO = 0; + public static final int NUMBER_ONE = 1; + public static final int NUMBER_TWO = 2; + public static final int NUMBER_SEVEN = 7; + public static final int NUMBER_EIGHT = 8; + public static final int NUMBER_TWENTY = 20; + public static final int NUMBER_THIRTY_TWO = 32; + public static final int NUMBER_SIXTY_FOUR = 64; + public static final int NUMBER_ONE_HUNDRED_AND_TWENTY_SEVEN = 127; + public static final int NUMBER_ONE_HUNDRED_AND_TWENTY_EIGHT = 128; + public static final int NUMBER_ONE_HUNDRED_AND_SIXTY_EIGHT = 168; + public static final int NUMBER_TWO_HUNDRED_AND_FIFTY_FIVE = 255; + public static final int NUMBER_ONE_THOUSAND = 1000; + public static final int NUMBER_ONE_THOUSAND_AND_TWENTY_FOUR = 1024; + + // 初始化训练时间 + public static final String INIT_RUNTIME = SymbolConstant.BLANK; + + /** + * 获取延时时间 + * @param delayTime 延时时间(单位为小时) + * @return 延时时间 + */ + public static Timestamp getDelayTime(Integer delayTime) { + return new Timestamp(System.currentTimeMillis() + delayTime * MagicNumConstant.SIXTY * MagicNumConstant.SIXTY * MagicNumConstant.ONE_THOUSAND); + } + + /** + * 获取倒计时 + * @param delayTime 延时时间(单位为毫秒) + * @return 倒计时(单位为分钟) + */ + public static Integer getCountDown(Long delayTime) { + return (int) ((delayTime - System.currentTimeMillis()) / (MagicNumConstant.SIXTY * MagicNumConstant.ONE_THOUSAND)); + } + +} diff --git a/dubhe-server/dubhe-algorithm/src/main/resources/banner.txt b/dubhe-server/dubhe-algorithm/src/main/resources/banner.txt new file mode 100644 index 0000000..0d9784a --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/resources/banner.txt @@ -0,0 +1,14 @@ + + + + _____ _ _ _ _ _ _ + | __ \ | | | | /\ | | (_) | | | + | | | |_ _| |__ | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ + | | | | | | | '_ \| '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ + | |__| | |_| | |_) | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | + |_____/ \__,_|_.__/|_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| + __/ | + |___/ + + + :: Dubhe Algorithm :: 0.0.1-SNAPSHOT diff --git a/dubhe-server/dubhe-algorithm/src/main/resources/bootstrap.yml b/dubhe-server/dubhe-algorithm/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..0824216 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/main/resources/bootstrap.yml @@ -0,0 +1,37 @@ +server: + port: 8889 + +spring: + application: + name: dubhe-algorithm + profiles: + active: dev + cloud: + nacos: + config: + enabled: true + namespace: dubhe-server-cloud-prod + server-addr: 127.0.0.1:8848 + shared-configs[0]: + data-id: common-biz.yaml + group: dubhe + refresh: true # 是否动态刷新,默认为false + + shared-configs[1]: + data-id: common-k8s.yaml + group: dubhe + refresh: true + shared-configs[2]: + data-id: common-recycle.yaml + group: dubhe + refresh: true + shared-configs[3]: + data-id: dubhe-algorithm.yaml + group: dubhe + refresh: true + discovery: + enabled: true + namespace: dubhe-server-cloud-prod + group: dubhe + server-addr: 127.0.0.1:8848 + diff --git a/dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/PtTrainAlgorithmUsageTest.java b/dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/PtTrainAlgorithmUsageTest.java new file mode 100644 index 0000000..196bb6b --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/PtTrainAlgorithmUsageTest.java @@ -0,0 +1,96 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm; + +import com.alibaba.fastjson.JSON; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUsageUpdateDTO; +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.cloud.unittest.base.BaseTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +/** + * @description 算法用途单元测试 + * @date 2020-06-23 + */ + +@RunWith(SpringRunner.class) +@SpringBootTest +public class PtTrainAlgorithmUsageTest extends BaseTest { + + /** + * 修改任务参数 算法id=2在算法表中runcommand为空 + */ + @Test + public void queryAllTest() throws Exception { + String accessToken = obtainAccessToken(); + mockMvcWithNoRequestBody(mockMvc.perform(MockMvcRequestBuilders.get("/algorithmUsage").param("isContainDefault", "1").header(AuthConst.AUTHORIZATION, AuthConst.ACCESS_TOKEN_PREFIX + accessToken)) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse(), 200); + } + + @Test + public void queryAllTest2() throws Exception { + String accessToken = obtainAccessToken(); + mockMvcWithNoRequestBody(mockMvc.perform(MockMvcRequestBuilders.get("/algorithmUsage").param("isContainDefault", "0").header(AuthConst.AUTHORIZATION, AuthConst.ACCESS_TOKEN_PREFIX + accessToken)) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse(), 200); + } + + @Test + public void createTest() throws Exception { + + PtTrainAlgorithmUsageCreateDTO ptTrainAlgorithmUsageCreateDTO = new PtTrainAlgorithmUsageCreateDTO(); + ptTrainAlgorithmUsageCreateDTO.setAuxInfo("untilTesting"); + + mockMvcTest(MockMvcRequestBuilders.post("/algorithmUsage"), + JSON.toJSONString(ptTrainAlgorithmUsageCreateDTO), MockMvcResultMatchers.status().is2xxSuccessful(), + 200); + + } + + + @Test + public void deleteTest() throws Exception { + Long[] longs = new Long[1]; + longs[0] = 38L; + PtTrainAlgorithmUsageDeleteDTO ptTrainAlgorithmUsageDeleteDTO = new PtTrainAlgorithmUsageDeleteDTO(); + ptTrainAlgorithmUsageDeleteDTO.setIds(longs); + mockMvcTest(MockMvcRequestBuilders.delete("/algorithmUsage"), JSON.toJSONString(ptTrainAlgorithmUsageDeleteDTO), + MockMvcResultMatchers.status().is2xxSuccessful(), 200); + + } + + @Test + public void updateTest() throws Exception { + PtTrainAlgorithmUsageUpdateDTO ptTrainAlgorithmUsageUpdateDTO = new PtTrainAlgorithmUsageUpdateDTO(); + + ptTrainAlgorithmUsageUpdateDTO.setId(38L); + ptTrainAlgorithmUsageUpdateDTO.setAuxInfo("更新测试"); + + mockMvcTest(MockMvcRequestBuilders.put("/algorithmUsage"), + JSON.toJSONString(ptTrainAlgorithmUsageUpdateDTO), MockMvcResultMatchers.status().is2xxSuccessful(), + 200); + } + + +} diff --git a/dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/TrainAlgorithmTest.java b/dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/TrainAlgorithmTest.java new file mode 100644 index 0000000..7fac932 --- /dev/null +++ b/dubhe-server/dubhe-algorithm/src/test/java/org/dubhe/algorithm/TrainAlgorithmTest.java @@ -0,0 +1,99 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.algorithm; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmCreateDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmDeleteDTO; +import org.dubhe.algorithm.domain.dto.PtTrainAlgorithmUpdateDTO; +import org.dubhe.biz.base.constant.AuthConst; +import org.dubhe.cloud.unittest.base.BaseTest; +import org.junit.Test; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.HashSet; +import java.util.Set; + +/** + * @description 算法管理模块算法管理单元测试 + * @date 2020-06-18 + */ +public class TrainAlgorithmTest extends BaseTest { + + /** + * 查询算法列表 + */ + @Test + public void ptTrainAlgorithmQueryTest() throws Exception { + String accessToken = obtainAccessToken(); + mockMvcWithNoRequestBody(mockMvc.perform(MockMvcRequestBuilders.get("/algorithms").param("algorithmSource", "2").header(AuthConst.AUTHORIZATION, AuthConst.ACCESS_TOKEN_PREFIX + accessToken)) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse(), 200); + } + + /** + * 查询当前用户的算法个数 + */ + @Test + public void getAlgorithmCountTest() throws Exception { + String accessToken = obtainAccessToken(); + mockMvcWithNoRequestBody(mockMvc.perform(MockMvcRequestBuilders.get("/algorithms/myAlgorithmCount").header(AuthConst.AUTHORIZATION, AuthConst.ACCESS_TOKEN_PREFIX + accessToken)) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse(), 200); + } + + + /** + * 新增算法 + */ + @Test + public void ptTrainAlgorithmCreateTest() throws Exception { + PtTrainAlgorithmCreateDTO ptTrainAlgorithmCreateDTO = new PtTrainAlgorithmCreateDTO(); + JSONObject jsonObject = new JSONObject(); + ptTrainAlgorithmCreateDTO.setAlgorithmName("untilTesting") + .setDescription("untilTesting") + .setCodeDir("upload-temp/1/20201202135732212Bp8F/OneFlow算法.zip"); + mockMvcTest(MockMvcRequestBuilders.post("/algorithms"), JSON.toJSONString(ptTrainAlgorithmCreateDTO), MockMvcResultMatchers.status().isOk(), 200); + } + + /** + * 修改算法 + */ + @Test + public void ptTrainAlgorithmUpdateTest() throws Exception { + PtTrainAlgorithmUpdateDTO ptTrainAlgorithmUpdateDTO = new PtTrainAlgorithmUpdateDTO(); + ptTrainAlgorithmUpdateDTO.setId(138L) + .setAlgorithmName("untilTesting" + System.currentTimeMillis()) + .setDescription("untilTesting"); + mockMvcTest(MockMvcRequestBuilders.put("/algorithms"), JSON.toJSONString(ptTrainAlgorithmUpdateDTO), MockMvcResultMatchers.status().isOk(), 200); + } + + + /** + * 删除算法 + */ + @Test + public void ptTrainAlgorithmDeleteTest() throws Exception { + Set ids = new HashSet<>(); + ids.add(138L); + PtTrainAlgorithmDeleteDTO ptTrainAlgorithmDeleteDTO = new PtTrainAlgorithmDeleteDTO(); + ptTrainAlgorithmDeleteDTO.setIds(ids); + mockMvcTest(MockMvcRequestBuilders.delete("/algorithms"), JSON.toJSONString(ptTrainAlgorithmDeleteDTO), MockMvcResultMatchers.status().isOk(), 200); + } + +} diff --git a/dubhe-server/dubhe-data-dcm/lib/LICENSE.txt b/dubhe-server/dubhe-data-dcm/lib/LICENSE.txt new file mode 100644 index 0000000..7714141 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/lib/LICENSE.txt @@ -0,0 +1,470 @@ + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + +1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + +2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + +3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + +4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + +6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + +7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + +9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + +11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + +12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + +13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + +EXHIBIT A -Mozilla Public License. + + ``The contents of this file are subject to the Mozilla Public License + Version 1.1 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + http://www.mozilla.org/MPL/ + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + License for the specific language governing rights and limitations + under the License. + + The Original Code is ______________________________________. + + The Initial Developer of the Original Code is ________________________. + Portions created by ______________________ are Copyright (C) ______ + _______________________. All Rights Reserved. + + Contributor(s): ______________________________________. + + Alternatively, the contents of this file may be used under the terms + of the _____ license (the "[___] License"), in which case the + provisions of [______] License are applicable instead of those + above. If you wish to allow use of your version of this file only + under the terms of the [____] License and not to allow others to use + your version of this file under the MPL, indicate your decision by + deleting the provisions above and replace them with the notice and + other provisions required by the [___] License. If you do not delete + the provisions above, a recipient may use your version of this file + under either the MPL or the [___] License." + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] + diff --git a/dubhe-server/dubhe-data-dcm/lib/dcm4che-core-5.19.1.jar b/dubhe-server/dubhe-data-dcm/lib/dcm4che-core-5.19.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..db86d92a224870da1d23f3cf07b4e1ebbbd70fd0 GIT binary patch literal 483246 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

<6u@g`QP@kBxz)Fi+NCQg>4xr*IcLRA!WH07)> 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

eq=R{4tXvZot;qX^qB)bAT zWjkNL%Z5&Uy3cko9NH$2hd5c-g$(!s&k?)j{E*?`NZsak-B#7?FEkm);Nc+-gw}Yt9m# z%f#P6s)t`93_1&0r-1lZh;3~vF;V0xZ2qTdk6kCVlPn+X!Inb6isw!8^9anHRx$MI zsN^msV_iPIEhPY3pmW{a89CtCHgh1y5Ho<#?Zn6%o&LKMdXC5&KNJnW7z6C86szMk zuCU^78d!XA%USQ^hUB==yNW>XADIBQo86l-gjM$fPbq?=S^+(sd6|EvY_67%vEZc1 zlq4CN@`t0)*%UphwCc)K_siBwbqi-fN^uoT(Z!rC^9QnY|A?>71JNxzEz^<f#%tVPIvLzb<%1q*U2e*jxPHHF`RKH{Hs`G3NCz zv%j{D7;@L&K?ysE9vFzKX}EwG!*Pkxk;SzBT5AHD(EL8%EVL~H22|x=^1mzpn!D~Me7_cYkn(k6+W|uo3>C9KIrH_=U~b#Wjp~N;z**!p*yR9 zAK(Sp+0$NxoZp^kVYoNvT@;iLM|C=sm? zAHelPN^$FyZaOi1jXw0Q(kt7O4qAX+nQ##W;Gjw5$2RyhCS8zw-KTw0_=K_SP`7K& z7MR)>lsXVJSL%@B#N=D$at*VTD|AG+Hpd^Q#K#@v(obBaQuaaE>sv~#(+zecR}Yda zW9*Nm8PDjw=?JL|%V}O?So6{vKwIgXBTT)&Gu6P0krvi#qmF0CzJ}F<3hF)?G(}*{ zGNacK;W6v}M*cF^{OnpAz^rEz0zaH~(<@*8k5AXK$%}G0rjMUdBynbQPLs2TinshV z*PFtQh?M1v;<*u16TOP*OVn0Leu(j-3sBS}uP{D?`00+&gG=WFAk?rQlV@&!ABSuz zsR5~H@8UC;HUb?#3edrA;bfm)VkPwwWgtq@q}3pYEKq86;Enkv$11iZ%Q9Nas<8zw z*qc{9BG0XJSuEc8Ga&P|NPSK$KO1M4mNufZJdr~LOLrv~IT2bq6Iwb7uDOphz9hae zZBfPq#$*~tKJq8>)ouaRiX=K@$IO*JM*1co-A7qnc>hg;XqBr9R2T_B3=BYdRiP>I z3(-mVTO`qK#pSb<$GUq6@OX~Nx@b`E&y114J$Q@`v#RT5jQng7^?M`ZTlN7G$y@Kp ziJ>Wu96#ZS3rr!%@ue+iRqV%z_Qe=@w4X$N87RFeb9NHQBl4GJI}p-lz%MR*g|b6K zSV}uU>XODS#2Gbl?fzTlS3Fl-@sh>po{?O2c6NasO^#dQT*17|MMJePDez4Qm@lV+ z^I#ocqLE(5GlOQRbmlEJsNa&w!cDmv@nRl5^D7qig2bqCT<|+Edtg1>!|!jzcs+6Cf$$BZ zMes9OdL+iR%M&oP4js}+1{7%I+CQj?n3hc-a_?vsDs(>sjyesD)tN8iXjuyODhrX4 z)z+c_O3fGchpVuxK?5|-=_(nz#3>NJ{xf{*pq*9pGv0ah#wyqoY}z-?JZg{Q@Iy_Zon628RF!Z1N5@Wu4$?E2>&6_PSy{$Q`$b7BqO$TMzYp}^bO}8xf zCwY+^i_7{8XbYMCja>WklisSi1yuCBr1U4hVraUofO7zIR@58~VIP#+OAK{KSiJ`j zU1)=0_{eKC%YFy=DmCuT@Er^SDqE!9qcdn=b4uZDk5?OuS;h)s>WXcVjalLci~$D< z!TV;rfzQ2AKJP3bz5Vm9ckB&kc-ai!(06?>zTODHyC14F-|`kw2$f>Kl2-==%VghD z8Bqy;x$l^+5DOOV@36M1_$werFdiFTlkrguv~aZ(1|RQQL^NP5&nZhlW}ul8nS&OsZQ> zNjIq@wBz)k+<&psJsGQXh5+h z>1#M61(RBPE6SWn$sqmi994G0WjvImbBXR9r-bS6C+9L%s!hqIV4MelSvLyp6^PWR z$D|7tl#-2FF2)&07|nh(2#5@wPCw$$p12DG(T>n~iv@7>j%IXIHV2N@el!f*bf0;N z*uMaudxbuCDBxa|N5FwiJZ7rAkIZ$Qsb9~b_OHvN1mQC9()o9$IL{DmJ*B>y^?O!n ze1cy%94#}t1I2+I^b$)EG~jA~R!HMX?`)L;kI{kk9$fLT$py41o}o>g5;q5B1q_$M zZVP>xFw8^i{fq5d)t(I2iW3^kiiowKO|`&{OFFdb$_>=DBlbEbrJOOPoJOULj;PB3 zbFHB*{c`4p^#R9AV>tuMJia-$rJTflmz*t|JE9^#d7ih@o*xsu?X03Kpeb=8YoSrc z!h>X{v>Ith8isj@U-0BSXsJ)4(F%L%dMzR9DFD9X`t6ee^CD1@#cd+6BbI^?bBY-u z67KfG?DIZSfrbvi-xIy%(guv0)NMKgS`2&#d3!&(fPnf3&QJ_q=8w02x%)Mv%PtwL zMxx{oE3cwGOGzO39Y~!gLG|2I^mzB1YBV@vcoAoj6PGA-QTYUk+z=RF5u9;WFGZ6f zOon1=U&O+fN-!503S9nW?#h4OANZ)$v2ok44LKz1j9ff6{yFR;jMA5mF-1xvF|SG| zb8Vw7L)Q||bLP^c)R;HBcZF;K0r_1@0HEH8xQKAt4!IaV#(E6Zm8+ky_Lit)I-IX6 zr8Y_BomZ5vpfnMJin-TQNxi`?_6=Knbjqe2Pjb0-$_Dw#EsZ9B&7JIU2zm&8mPsDB zELP#QK&n0JuVqHQXpRFp;`9d$SOtJO(19=bNLELsN9U!>0=SO`E+{2dwS<$M$ny?i zZIM5PI*|MyaWETrgMk@ek-#n|VZ4tKG1EXSc4#8%_zcf3QiiEvpgY%8Dzn;ftw1T1 zYO+Vs^T*|Ov>N1;C{D3h=BCD} zw&P>Q*<03`V(j?$#%r*_Rz_cuRbUB|O1;b&1M`N#OPDCRP0+N3rE%J&A; zyajCln8NzavyXe>6gj(8yI=qt`EVT*mU|qtRFo#ueH$(AZC!3X1MXsStM!s-byCsruCz8>JDu{d<^lkCT7wQ@^> zB2Bt4t2-4Do!$8z05TpxJXJjrrec!*Sy0G6yIX@^bB}T&Y=uE`B3|4(m*;wdy_jbf zHDOxD_&6s*SK_FmzG_QGgwM|zO2RL)ApW*qC)Ep}6_vy}AI0fdU7>+~1v+pPFFPT9 zokFIa$+qYuSvL^%adehJ5p#{we{V{5hpc7)n`v=@hQ>&SJZ<~vppT{a-bzqc$wMix2M5!S!7l0Judx@wm!SI+ zTt~m!cjog1#9#Y2LOw!*;6^^#h+`7$1RlyP-@Qv!Q8mCcU&^M$bm~duwQ4I(0=geu zg&%e%XCm!((UAx>R6`}6H1I)#D0f)h`~f4yL03$w@FUp*^G3PsaGK6ic~)wwcdj_6 z8Mg6586@**a?UUfeA}b^KH~a2CTZ?Fn;h<812mpJK(Yv|fAwf`ZLDRPgnl+IVXD>beV$)yO~gV>oZ%#<;%LK{tU|+n zLP6zYRF+;JGJKdxRIJQ4*s>^>xE2A9H)~bKO+s(u5-sb?(|qq+$Oe$nqAw( zS1oG+`=NUE{Hj{Iw^rY%W$fXcC3aP_e?zmqT-afamPNb%=Lf5p7qwb;En}Z-8@}Tr z(k(63XeCD_K#Aj)k-U&V*Fg&BVZblLrU-)seDZ4djCP3%k>qdTmgx>zQo3kmq9ERl zOv?O7aq0I`)ENr*z^x@seL?CYlKrsz5sZ`bQQ0Ar#^e5(`c+du8~T)|h*E{cjSlWY z--z(Zer;XT^r|zhY;~BcXg4hy`G=pZc*e1<7L|Co!&Z?g($Q2nqpgts|1AhNByA%R3^+6M})vfJM(Y25l;VV(m8vL%_Ie z-6!bxg9;432j*#InNoU_EJ&1tabI}yXZucgTGw6X?#3(p)Ph@B;ocRiM*%FIjHFf4jiH!Xk_0YVW%BM+0iNLc|+DL-{~Pb-F;SvcGV|x-xbZ?$(rJf zLtxH0B0XLOScd_%0^+~35o@qw6;y5ssrlKC;D2kv*`@nN#3{DcQFGJcev_~yI8Rmu z7}Tla7M6{{za~1A$3^03&W>Kw#kr0x#aZ_>y`^>EU&9BBXyRZ^F<`8fp7EvmX`ff0 z05I{z!@W7m&7D^mCi@*t7^k>SYTnR6a>OJQANJX4hwGCJ!q15`D)y%}mp)WXKKUh# zQwt3J5k*{D@ww!&v(B{F{CCd#-_UjE6H~dnXKfeAFs^HQj;XI7>;h~#25DCG(oRaj z2dmPLXVKCmhHHmus@avWMOD(+Gm4A*x<5zcwjZo!lxdV&hVzWois348%)ztb+&3Zz zyP>9iSXRC;xZ~(%tlOc_?q;#%$nI1ymQBnmGzJQ##AfMZ-&HM|S+!vfbgY}7=yn-e z@x5YMCVgtzqID*z^oyOSiL`+>4~yZOHEjTn{==b5ncm)S%3fK<-;ztSUPithnW9+v|ntn)5{5QrGGx5P!VHM zk#<>TuB*q)Gt-^HzIVR($7s1wI#};~eXn9_v<0LCGT^A`#(WvPh4JJ^<9|Er|bZUFk&kQBA6! zH?sB6Sjdctj9O-oQ$ggtEmfRGOtl`S)gQ1_q1XGcTKUVBpJ1v09O(5lhC7% z?ZA#Ze&!n~R2t_x4lRs*6CuW@ZamP>XaE=u!b}$JUvDQgG7VPW4t=+*cM~Unob2mO zJq2t_Ni|g4OS5uj_+6O6_DduG)vdF$Zw!AA(m5g3~OLD!TraARFTX5gix|x z9XbOKP&P}n?;c@lm^3l%5#q#s?zB-EiEn}b(73)?;|?bNI5A!{|8MH-|D)zz z!OitY^#0#Pu6ddeo;oXN-}Pi4fO{Fo)b&9pa4fhsUKYt;M4{{O;-ihf+E6a%mBswi zf9``Pl|fp}QriTru)^RgXcP%iDS4a+5R(^$tLu*+tiU}LK(NBTw{Xa4>JP;vyBcr_?4vDVMB@)H7<_U(k5qNdwKO&NppuO z5AR}RmW!KOG{SsD7?&Wev6DLQb4cbNALoE5{3PY5Uo!gSMiy@vY z1cOR-Jw6iLWS<~M0dwI&BggO!7$-Y&HsEqI*4_yYAj9%*pM z5gRpKRvxrii3B&+UN%%^Se&-=tT@rgTTLWoXv-Ivu1eB%K+{VF{kK^gn1p!jdZ4ff5rqO))6;Jhq>9K$w^$*$y(f&iH79I# zMu-$Ao8+XrtgbMdi$k=8+Kh9;UECgv3e3^9MNzwvlSq{;**0}RfTLc$pkvBKdw8&9vEzyG zdqj$Ck<+nyJfcrS4&J^^e0D+-gY^S~LvnQ5O-jNCgSoa*V11ZEBg0`_bt5Uu6!cn{ zq|kVjr9x~Aa~wBN?tT>Iy89#9L%E5Em4T?~5KK;}FQ`i>=sCSI62h2++vt|tm&7{4 zt7Wd2BlN2cRnpWh%}KD}#vykjj*2}Z*HbDv7UVE8rD-rNGOj&1H%8>4ai-)53csOb zDX!K4-!fz=^eiwn29S@?csD82)G#p!+|wJ$cP|&Ofw6P)qu{ZF_J?{@$^}eZ0cr6y zX5XAH{E4jLwOk8#L{k53q(B{jY7B6>LKFUWz^}2~hzwdzBp2@qh?Cy`>Fj4GWVT}hUK&2o+p=b@~; zHD<6M&qFR^kh}(;CfthdHao%(L#v;9Rte3DnC|UR82;kM9|(b7;nL^eXwl7O^?no9 z@JbnURL})~RnkTwjCx$Ncepz+x?D~r)5!g@4xt~MAZz% zTfXmv2XPBpWn;Yxm3A&7o|u~qU{n_@OvOPsdqvJSl~~xM+S0VV;a0ju!&8WIBql{Q49D$d^vmk`guZh`t&9!k72U65x%OGwY3UdtRg8 z6Dx4`LsJp9D-kC~juoe8qB2)fua*5H1Cg=M_+5T@^9u16;%n!e!IUU9uv8)A6xOSR zgZTCj`QW2{sA{RG+XQmsocnzvMj%DoQ*9in2Aw+;Re#AzwW}dn$o-*21i5ML$AvvK zy$A1V=7u9SSxeJwm!fZxYISB$W{8iBmz24Xg}q}$>K$zUaeWb)a@7ugCg;Bya($#2 ziq+wYEm=v-l^qQmSxR+^YctsxeZLigl*G@{&chy(wRNCNY2IK4rPsm5X^c}MAcLT$W{NzDsMpzAi^oxFw)%7;we;irS8g?3nk% zqe$;4PuRBe(h$1HC=t|>>!?*V|LIoyh-G!s%;=*iBIAtFS7PZjVw&jHMm;w{aK0(U4cvC(ffEd{l12D-M26O(+|b_&EhBdkN2bSjbJ3!FY}6F?YMc-Kp?&v zLNs&nl?S1zHN|*ioiSeu%;C_l1xBVp%!|_{m`pj1n&D_yxwBNUHa3kaPucu-8?G$X zYFc?SQ+^UnG3^_Q_HWKWF+X;zC+?IlMSO|7a{rp{id=7Rd7rdRMoGQ$jOw`3duh5> zHLg=%RC){-`9;nJJmU3fgC)q5>gqa2y!mc5t9&(2;uydxmpgu`;C8KS!5OjkG3u{J z*1KOSQ0^4sqY;(^om>yK+{VJ?`hsn^Xq8qIiZ#~E?LPBnEIH*8+30t~f`39t?FcUK zZfb=NjICGm@?~(&+~h7Ub%$4cHD!a>^Osl7Fc^nMb?5WR)d(e}pPNF7ElA$u2D*0? zAo->M<_)K;)M#3mc1nPMT!5d24ewv1@{Zz_>H63eae1tIz8*(_5O(hyR^pz^a_VJ! zI(PEa@*R6Pvs&3nR?U2jw& zhlnSC^xYX*EYITE8)=f|2jT}aW1qIsJ!@sYP_|HH47H~sqtNj_^`clCS;+LGjHoPJ(N7A&yJ-EU>s>H_G zb@K|*H^T`|GH-=CuBn)zFR0GyAM{EWJ2Fd^3&wm#r};E#|GYBN@5XhmZl;-J8uj`) za!gV%E^9E*j62Pk`RmfIazE(iic7c|*7E&e z-E|jnxe42BMYwY7o6G*?o#%jHFs3Ni)ZLnZXrD81GQ5yWJHg+^UjQ*_}nM zmVZ!J=iEJV^>H_9xAJi}Zx31vdTFrdbEF(4h@Ft`^DO9t2*L4iIHhK9Cy>8Ge&}Zt zZD#!(Ozq(nMRt6#`CKf;|7PG~cXO7+mB98QHw)0Fd(gVpA|Z8Sx2vRVgBoxKP|2zo zBGC1Qr9FF%-LNlSU1`SgGywZJp;rF&#AJ0`%38YaLbs-- zD$-iwoo@e5caqI=a2QM}w{d8z4B4n-i5ivDV)@-$o?hfoYVZ_7}248}&KgV$(v}0qD#S5EjAREtc=3G?Gq};!`TFq1mW!Ag= zO)0fR6IpCHwEUcPg)qR8T_6EEbc@R41f{k!f~P)){Z?TKmuX_xalr2kyIqh^ao^tp zdrl@4XYti5?4vo8ia<8cp!DwI6NGbQm*I}lD_QExO!ynx`c4&Hwg1=3jo=fH;#YN4 zQVJ%$%1o}?CR%NLAv=Sn=WN>>SA5iwzxeSn6Kw`HU#P|yW5LPCx8NpLMTXfUp}y2% zf=^=;k%;6|!d|_7rXbB$)8brGK$qe^{l>7!%nQNj!@-+xn|JtYpSb{QU zXnWR7gcEz;@Y0>ullv1HJfx(q%vD3nZ;m@=wcF00PW+6XsU-E&LMjD zOBLc0{J#1{EY}C2z1(3y3i~Xp%|V9gmOOcQ|F*9WGq5?bRCM_I&q2-TRas9J z4hRSV_y0Di{U1H`|23kCd77C0r>XwG42f|`dNMddsNuUQrp;_|W7SpK0@cdVab-c} zl$puOpMjDM7z(p(z&5Q4h??8R99>lvLn18_nVw8L=TG1nrO^iO~FJ1*K z1Zx3vv%sni=_1fp7+Jiyrcg@;qaI4?UAq68UY1gJjmv%sKbJtO>XMYM?Y77sL=BEZ zjy{FMxr)uxa~vvy{Rk&Kts{AjuIZhYp`H+&7So_ujS4}EScM-1r$O8O;eK?I)c{XIfh(@CDoPD)5^h#79Hc<0NRwrtQ5mM~iNrUtgu~c@|^b z%^gg{p*YKJ!|-+1%ig>x@$F<0sxtv7U=Z~Up_e}M_qCwKOgm0wPMlVE_zJ-L^OOl5 z$BA8dPi$ZLhS8>7(g*};bsCiWo<$W+790L7iW6Kn7^MK{Y&`i3kwdD`8%=X~R{iJF zaNO-!wEPBIV>6;^5{EK3EHzZE9EOy9#Em>x1mZ=p*A())@&v5Cx z$=&}*NGJf-9q8qR6_pjGYbjJKe5m@Gp5?Y@*Qi?k>ba5Xrk&@$RK*PtB}~39;8-Ac z)LcuuoV0fD(x;y3k{`|3$vXwpdctc*j);5itVPxYezK`*|pXt*vbM$OuNlQPuFJ`Vq~zqg*^!^#ZTi z5tm|JudUZjE7$E0o$E9KY(OS8# z=8XK~6wG_^pSt96$)SA`@@1AYwR4xge)^7xk5<$o(=qzxV$T)VIS;J(7h6PgLH0XC zN_dCHPCLxS%at`GqC@O&QcZNm^s&m6Zea$o`1a)Am1vdD4MU`d)Wwfbq^I-2l1ygW z04kn7!59jgMFh3!wHmdx2eZAu#9R;d+cXmc7t00|d?l>H3~)@ZJ0yP<9g;Xi-pUoa zMwUPUiO0LmpUzksPan)Wh`)9dUqM}rU>X^+g5XAWZAx9J=kWKKi#vkZb;Wq|FWf?W>)?{#7sXB`tUL2ho6V(+V`#r(WHG%^ zzUXi*asvm`6V|K$Cg+yl)VNpP#AL_z@5sS^`eWf~FGz%X$=0AZ5X8u}k%>&obm?mb zM0(?R3|&DCXBdilY&@4jO+yZXl0U9(|EFJ^_XKB}=cgXG!Tx)T{$Fd7_J89S|EbCU zevm>5AHf5dV@Qdn5J7@WWtJH0VJNUw>uHejlB zr4-={_)96y0==}fDvE*Ubo!0{;p*+{^ppXpd%XooWGggD7(^tr%I8k)D7B!*bx56} z2lf|Lc4!gFan)@mqDNabdbm+mgNh`v*!p7#Luc3KmdnjiUAaDLbjYQ!UW+gmBL`x8 zXxM$tXxQz!fAd~f(>8r|`1B^`XlB$MD!*Yc;|qFX;wyS(qQX)q7hPIgbv9R-)e`9N zK8)~~*QVrL983Q!w29d|Ij|N)}iE{geKJHe&6-xfi&*%ZP8p)WW z0(Z0jcwP0Nnussh?|W}jz0$Yd&NzfnFoU`J_KvqOG?KG6X zwYRc1`YgkHELmUhE3k z!T;vPP!ZBpHZwB)f7#S!sx~T1f~b5lGQWrf!K!)15E~E(Cq%;T8}1R!Cry42PmJ4j zr5fhAFRI>AygJ;%Fft!%SbQqQdx+Eu$_ULwY|qT(yUgYLH2=UZzQ7HkYpBonl=fB> z5eP#5B?jzyhwE7R@pw_fMhtQ|XqPun>_LUQqp>nRo?N!%GdC@b1!XR(Z0&nbJK3^m zxrz#ffh0teTS8~EwG^fqDK_DsN&Jb;KXn&c2jfHXX+NZ2>)1cKRDhLBf9)_r!?ni> zHX_B0-i?o*6n$ZBDasQf3BCsp|K5OA0s{`;r;%WCdH)g3haUfSp-Dq>ryYHWZ%}jH z3v#VYF(SmQcl$|P*AV0IMt!vYJ1i2Ok4=nuzKuC-%lJM14%(=7R+%Gt039lTnAI~< z7wM-z{fVwA{UUjSZJ5c>I4{Kh+joJ8c(B=n+&h*CY&h8_wCJLXt@J%^kE+W1l)RN= zQ~9HL8ZR|U1U3pg;#I>R{KlDHjK6(MPR16kS<0yY7yv7_aljQ~;ofOC`Jz03ftv|d zt@kO861k|-A{SNP5Ii|a#eu}zBZt0ef!QTfv6PU>@FZ(6(`Rle&$;E<4|pgWuB?#= zpWrc{rKZd@!K@E8FsyoenQ>h>adyBeHOw!1aE4K8k}){5wq(Gt?fwiZlGnzp)Rqkg zP}8~EAX!wmkJWOrCDCkzYqFH#(fv?+#0>+z>*4RKAX3%{*8`bim$oPaa5u;X3#iV^yeRMS1$GB&lo( z2x%N;K6~k2+Z#iiKw^OS*=MOjiDvUc)ke`n~)`;u*zik z8M%iK9@@CAcd0@c05YWOG*&wDbx);r_q!4U(qfGMjQ;)v`H#xEG#G5V`ay6ALHzfY z`oF?s*#7sGqv~vA?_%!YY-i?7s$%Bq=J?;f_^%DHUwkk0kRoiD!g8B74dk^vd{?Fr z2L%$SkYi=!MXUb5zX@BL(gibv5Vu7m`a+5jETlwSTyNJ27jOeWa1dl56xMT_gxKH^ zTze)%-=4jy#H-z+V#;p5S>~q+K_SpZUdR}OMUgK(>$#?h$h3B$q4FN~>0GBu6K5as z0=VM6XoF8FvS7~Muw+F~U(F7%kUO;TW!a&^HdT1bP*?wcgRL>t$`Pg-1w-4zxG$vb z^*E!o7I!gXW8~}k{O9_7Qpu6`{``BKpYHO1hU<|2f3DAeEl010o(75r>NbagNO%+> zDjOtn^?YLCT{9hcHKnrBfHftwUF)!E;GpSGl3}oV4T9gVa zzX|8(oh%{~9+SmaGS_)LFPF`8{HOVyZx8hb0lyr9>%~0x(DqXHBm#^4MJl%bQzLy0+}qP+q$z@ zS}TT$>Z&oT*Erdu1pP7cE)Y0Takk_}SN5{gReyZN+f-S&^uROI>A0BewZhv}EB$qj zuT`a6q2H3jjuh!&B4m{Zh{C_MU1hb4{9aU>6B|KWer$I`x^rjg)jAwze0fT7cP+UT zl1O%c_k`V%9b23zbte(KUav`D{buduLwFb@6u6rR4znY$`s}KWYS-i$|J4Nx8N2BU zBY0zlnrSsUkFkV{%KfV=C!0@+7g81_AO?9S>xoH$t#~n;nJ=rkAiJnDhh>>D#jc)e z#-<0LFb<$FPTY#v9dW1s(_!ZvrL5o_X;9BO2sms*aLVsC3TIjHi`s~0k%Z)=*~gt_ z8Tsy{5}8+}qrm-mWQ}yU%>U@5+(+KA;1q?c`Yn?8d$^PJTirJj2{?m{lt~(`PvHym zlia|VQ>}thrZpc!aE4u1!wJdc?*RZm<0n=_Dw-H^_(g6*K82;R!*hP4(v@-#TtuOS zj+lJ!SZMnv@Sj>EW4Crqf;w#6xDW%sh(2RYfi?#{V*wT-RYv~5_|30xh+w5DYnbKa=1Eoxl43z zRt8+0Kz9tkjamsMiJY=bM-0RVFNZZskL(NTjm<%XJzls^hXU&}fN*DBahbOsR%HNs zxQv{6aokNyhl+DlwZ)9H{btd^(=pM($)wX8JBvA%_1X)rh;Nm8_|~7BO@z`6FnNf@ zb%VZA62hKM?iZq2UZogs;}b`ywgQQn*PhtsbNkkbp=ijd?wwIQAd`$cak99(~tx?(B<2&n!Xw9e6CC`?bB$q-E`aD^D|D?q0EA_Jx#yz``Gjq}ZL)be9 z=@KRD-n(tv-fi2qZQHiJ+qP}nw!3%Rw(Z+9bG|QT&OIk?Ma3IY5f$~|f2R-&GdR7W&0-D&%y$NAJCzUzw*1F{fO*oA0iR@gis z%5Mr$q1N;%k;m~l%)Lb3Qzr2pu&TG@N~s2jQFTWx;6i`hdayWfisM*`49;Q6IKhfNA*Q3=q<-O0(D3hjROSMxyjO+ zJ2f$sld^bBt_E=}LSA6Z05_P*>aEa&kz)pOl{Qy_C07w_#%rna&U5+?pTPb`ZKiWy z#e;kQnM#FB0ovu8`OO0h=D}*uiJfbZ3iJM)>V$-Q6FHk*fJ9sBr!ybCX9>NNG<8)R zsA+^oT_1tEDy;3|HT-ykpn;bi5AtbTxTZS->P~(v^Nf=6?%oouk*Uz7SvJo~lJr+V zDknu*0jis#IhNlO4!?V-q^IB-&8epj3w8w2x>$AW9woi2*1NNi?G;O8c?Y!<68IbR zAOCt)#xlA=KZgk39|HLQ9@Po{=cumgVE)50`TrnWRZ8nN3qP2e3^x0Obe2Zx+y_7q zAaM_Si3w?bF=b^TxnE(qes1e*lJ!GPl5z`p%Zh$w%+{z2KAw5hO@T;PEpOS`>D z+jm^z{QBBIXZ?lMn!yh!juaA%JP+;2zhb|3wN<>1f)x#&IlMH1mKP~(R>N4hUmbY? z*>^UwR~~LZDFCW&apE}RLKL!>17nTrSW-u;^{Uanu8r%EqJc%Yw3!^tn5u`axH;H1 zQp;B8OBpS=dNxE@ZW1@3j--fMOg|92e&~JWwsFlStJ%>XB|gS|I`P6ISg&SbBHzo7 zbWAK1|K$My7;!OQC_kcK9?*hjdTzFL>9Z57#9gt zKZjfN;R|6v*Bscams%;Q%XNC$B=nNm!c{J{2$bR}jVs~otHkuz-aw5_C%sF@SnM$g zDnxVucFq=1<`Yn&=1-y`5jPjcJuFZKJ@H$kZ166&1n{%R+9pmM&Sj) zCKj^k_ym~VVG>a4;UZ9E%8z?#frxJ5H&Y&f_(yJ?A~d5~zcHUFdDpZZ2ul_D604H9 z{A3ER|5VL1CbD$w)tt4aR~2@c$K(M+^WK2M-GM!xv--iW?)ImU45e zQm**1MQfU0TY0Nw*08emj`dT+Ytpc+T5PtcXlkOx6RujbSj^wBy>xV75C@%{@$^mM zJ-TSXv#jY_YUyrzf{^uc*arG#VuBBaaRa>5v=gW+H;ZdqWoB-ZvDit@)j&9}GspBC znRF}0fQ@EkyEwdI#DKdxn+6TFH{=`vI%x|*B8WnntU}Gf*+`;<05haPiE7Knxsh2Z z!B|s9w1K^fZeG2L93Gn9CaB1E$>lmAwD=cA9h{KSj|z%N55fV|miQ(IIZQa-5jcCV zjFQ2<-3qE8R`8Li?#Rqk^5x;?sp|3;)xQ#!Q1(|A)35-~FeHOvUb9_3(cj7a!y@d0 zFpfcu3P3g~yTY^9Dr${5aO;W{MY zABOwpF6!@H@N5&o@s%#?8&;SgBEM1<}ru{8B%{Dni{D58c|P; z+ILSC3`2;tgdtrZ5Rp^ha&A3=+!5m6B)L%hDPTv1XDo2y}`Mgd)Fc*&B4WF7-TCrj6kH$)C}53>Ib zmh3VdAJ<&VnH{V_CCB+!{wF+QQ>`Q0Ze#v<7b-sglDSThS^rBdqu%vHg3Sd`1X7#y zhK-)bPyVqI`$ZvgjuOw$gkYp4Z%#=6=P*D|lq(Z_q$LPNhLSqdUm0+MuaN~)g$qK4 z3Kz3(;IxjueYihOjdGMwL_6-)Tu4Sc&om=D_z@hWwoia@^mp6m{h8aK3e1{b{iaSq zz*#XYO3}Mb<1C~|1B!*YIKi@Eh%>t8W5Xh@rE*1SGkUTlPXRe7-8g<>v|vXtER3P9 zBNKM`xsN^bS~A7S$5$F)auxmD8-MGE1R9Y1Q!-Wf~jwbmus8NIc zi*agX-P$cz3fLoH3NQ#U`;Z3?Sc1#Ikr2_n`hFY}eglTjT<~8XxTA3t6 zj;U!lGAtf3E|e1E4$hle6BLC-6t}^CS!54?vHa#LqBd^75zvv^%b6SYtX(xm`)@Mb zmK#PcerYBMOLWNLr%NNi?&;GSOn=hbh+oUnrkUhm% z1{dg+J%;W0*@wVYo&j#zu;I?l*5FvbU$W+}paS;vZ?&@mj-LZ55gb=uZ{;fTN#lai zZr$L0iN%iXb8C40c#D`7oK2k?rWK9940dZ?u*}E!T5N^v+}dl;uvnbl2z8onFm+6G zt->siWYH#}2dORwumC#Ecd0)F>CEl2j8-aXmgdq9!UhPAh-4Z5T9B2?EO(dlUTeIR zR6mKrkXSBzqHc}!h+As?HCW-3V{C-RqVhpkUgGgG+a(efpxJGn(gJ?7ZiaiO_KK8k zbu@4+H1siLYS&gA6zJl=$b}Lj4iqsrXkvKE{f$sV!XsEbo(|Nk)~z%{4Imj1FRo#| zL>#}D-e;746?^Kn>mgedllwTR&^4wHFlI7G?*zh_ABsM9OIPiE;5gP`)EKERM$$ia zcKO>k36IJ*?kPkhIQ?2;#vh0+mLyc1pKn-GfeZ;)6eel+uQFE#Ljl3U4_eUDBr8o!4VseG9u+cXCrU$2{2k5?ynUEx z#vu6|Hs;AcYj4BG71QN9O_j}IeH5f>4*5z9m1@4w6w2?TK26h<%Zgc6W7r@iDB>%q zYRz3ll|UsL461GsqZgDaZ3}x$^&vTy3;IT(lMRI`N0#}Pin=ArEi$BcdvjEsZc4?9 z25!IRO1U@Vg@0RMVFAv^J_W3fuDg0x@^4Z-&X4{j#@sa*%S2I>bE6zP(?vwqk*q%w z;baMB2}+1Nl2Ru(1vW|=`dXY;4G6SmUHzNyy$uA*EeS=!z7XG3AHBtxxwW!WshdW# zjV*DICz=KGSa}Z7(w=PRY034sZj5IGG@`2_P@ob7i?wOw4Yq5}0PS5}`8^YoS#Y}9 zsG)5UJvgfs4pPydIX6GifOw+FNYGNmi4%7(`VYr;Z@2o&;Z$diD2u zmgaS?8U#sygDHIxUAtRcp{H8e;$l@<8KW!9E3M=-($bsyl((C7YI#k_UK+7Z@{Fc~ ze~6D?()8V-^L>B=0>JtzhWXrF1a+H`=qE>eBMn*QCwJDZn4DTa*rtf^YdNI&tWU&f*JYHq+r#u}d}6JZ50 z@)qsZFw|ZBGbj;S6VuozG~i9N-Lx_mXA^CvYsl3}UoS0sRr~XtH7%xiEMj`_j>_2q z*uVGwCvc(lz9^uaX|k%u+9Fd^abmu_m^)c4#6Y}63`3i{rOf52vcUqC%9ic3Qn(4G zgjy3nmIH1FphT56hquS2jImdScEjNUKj*r*8vn)u`j05JK`T?LhGUwK#~f|K3Ymi`>w`CH7TVE}-y`D!>&#*z zG_!3roHHAR)CF3S55In)Z=?BW>5Lsn4gsny*Z82|va?gE=h&Xr?)WW`RQelvWW&88 zUGmy>R4Q#6##Gn?K-`yN{s)6~X3@8Zxo@e0i`dEe+yMErnXhbw$HS;>j+Jkd!y(wB zpM_z*N(QWix8{ZE)@#P>mzQ)A+1OH|Ce5IW&y_SOAYz_QI zLQzldTZ6LvXGUom1BbyWKtlX`Y0n;Uf0Q{y*TKnDp*K+l!c53?6u{>W?#2%4;i=Uw z>c%AC*i+%$wyVr!>e{je=^@o;bcUEzHjj7v&bXkr@R#rRJ;Y3>b^F)kY>wfhY4O+O ze9nHUhoYxxCp#B!{#@oI1&1^WLmDS)bjE}a8$#@7rEbW5R{H5*>(|VFl7%84?7nj` zctYNtYqwW|;d*bh-Ys&k9?h#$&lJzsZpy>1P)8Ahk;$oJ4t&{f4ufnYIiu_ZGCVIs zcA_q=whY^>SxR`irb0A-#A}*JsUK6$hRHgFHucOaiLnm zNA}TIjN##t(-{s}fLk_h+~x74!bQsD6jAjc;{)Zm3?kX(h`tvVXfBNtFKZG_g`%L*CJ&n*s7DnBkvrDKHrx6pnS}LbV^6ZM^LjIyP|}r zot5{gPG)U+VPuA~hlkzWxXl4mBJWBqT2sZir~O)>t9?~WEmx$%gZdxL;}=oH6Fy2$ zqKoZ*40~k!J`8cdrUbQBi^hy043Ld?`&SGAPJlEk?mkpzJLYUcUeze}dvJFJpT3`0 zZf~%2)nd{i8G-JQiT&CZ0sa~lXG$NKyAQKxgak=UD?+hz?eX(=y72y=y|q9E^_ z@FsTv$7l zOQuP#y0Uxrj84%OFsOc9Cyveb_;HL+vU>NTgO0{|%F&XISx#!Ew^X80z9A%ABX`H2 zv_lrSPhG_%)5|m#8d!u-cCA75U_)LGS$M*2-GfSgVb;0A0GWt}A7nDc&-6k?A0%BU zHb#??Q&X-*HGq~{itez!UN8VkUX)dPi-?uo)tYmk2aSQdM{k^CG;UIub}*I|kJn0! zgMZh8>Kok+;{ABRrIQ^1VT}!$&5pObhWk;Uo&{reCTV<4Hc?=!B>;rJnl?%y;!iaDCSvK z53sebI-$O-+)n9uRbWMF*&Cus8-ljel}Ae2K0NIl*#I}LMqIlDia$jwd)6g{J-xg0 zp7g=BeQkB>u#9`whF;=+{1BA5%@Wl%bC{A1pnZMCeg==W$&XXCo47 zGoU*{+je-Ub{$Ec3~;VWynTgiynkt~eqrPGH@Ts=?2WwO;?n=t(#sfR-j(SrA_+0? z`t72#N8``1VCHzWMyuumerV3e-mT{0NJlqSFhIKCJK zOAyzbjA82>tMM;%FKDOzib$f>67xN2gNqSBXT{S2w??RYNxc37nCw-Z^z)LWZoqBa z53Lc&rpLycAjZ?N&g}zk*E-d^uC5pBFpj1~?MBmQf7A9`Y6*`zs1&*Ri0Hx;1>&qr zvGNB+0srj~e*YXgJEdD!Jhx`zYdH5Aj9ZRn$~m)Y)JwNZ%s- zTrxFv1$cbm)_JnKCYyt#^rAvCPvf4Jmejk;l7od2h&NKPQ{IxO93!3&kmv!$gDKs4 zN7;q-nc4N8e#{Td^INQ=CsTXeY(o(bT+SO!k9XJBv`jZJ9by$7LeMWhsL!~0ouhGH z$0c31H`UKTh^^iVPz0>DeS0#xw8a_iiUZd>z)v+lq#LffzH#{1J3<>mhv@;#SMIvq zDW7u)~MrRai>N$E+zhV#xWPSWFFpe&Tz1L$L z1d7dg?;sLmb~yR?R>(gg!^cKtqz~U*k-*qZ6)=RS->DS8ctUw?RXtwcr>%73aGr#J zUsTO2h-~$=-!g&mp7aNK=?+VOc0DH@z0;ib`L<)^3bSw`qqr%aGFAo+7nvxFYl9$~P%_9Bbx=ix$6IzXZ`ve!1&(_1 zgtmWyHffL3&G+WeDo#kUw+g{uS9xpVliR$2)CbV%JGTk8D{BMj>|60jXgj7BIK?B5 zZc(r98DRL(Zhy<4^DaUQWx1n8S6WbkMd@KycVzaUrGahV6G&vx}h3wVsX~P ztCvdJQ|)Psb>FU5kdhn|dELbkl*P?9%E=?8ow;XqPxIyhDs@JGgn(6TkJIxx^1&9a zhlpN}nH!WlvM7m%#YDa&LP-nD?3Yi8=MktDnBujICnpevCX1&r?lLZ^J}!-0(w7{^ z6ExX1!|?nP_ozGVFYLeDWT>9k-0_AsJYgemk0$9iOHXUk z+tk)tB2rwvFby*2VlaXhvtU|M``v2KCIve%r{HI<@CV5lP$rr1Z@d=Pp5EUZkj9iscalA3T%gg zQJ_FwXO9YwEaACeejC^LT&IbYzPcN_%Fu`u@;Gli&5C5P3n=S(Ob$0n_e%$6E2CQY;&;> zT>ZV}^B~hShxTPd$M%+-1&nYVr0^{spo4;7jMkZ7_Yf+hP6Eo`tUGl^bLvdJ_MIJ* zB5l|)>OT%?NfMJO6rf|`Y=SZ-^j?s*mds{JZGXg=5gBICEnowUc@T2*4i2TqVey^i z=9$(1aoaV3%5^}Xu1^>u;CD!j`TT$`msGid50)E@nyfL7&ZMHXq$x8CO_aXQ;uOzN z0GJ!Rvsb^oe7mnv2ifDVQ*ha<807k?PM@n;LEKs&Y}DPbjxLkll%>MD>RTDaDrreF z?&>P>-C8P7l9;V>iY{X}ZI)bCY-pKPmVQ}~&`KN_^f=+Q*7J&40W}oHaiC^E(aXq` z*rVl1onhd^FkKl?#fvbFA!d?D)~Tnvt!jnb8?)2eQKqWNjHntXO(;j$Req>{39yhA z5Pg+{66LpyZkq`lmRD#i>RR`UNo`X~{lO@fMamj^ST$Z?`#q+rU5yE(J3TVkela5A zFe?f^4+3oF%`RL7w-K;f1c%&o;8Ck-(9l&R{JO<3 zVnq;oX(49fCa@b$?4KC?}9?9n&(=-3X&r5_<)Mk|^6NMJheWEnQ)3_aUGC%$A@ z9%L78)yri*Y(5_V3emB_0FeNb-dQOjo45>3uPD4s76HQ%{M>};uFdO|W_W+~OkWRl z=lTjx3e8rnLhxiy>&hcPMZ7fa3%#oJEMQIX$KwFXWu|DY@%4@YOAtuXB*T`FYksx! z*2Y8}si@zYED8N{E!#mj?X;Xr@o5ZedMb4}t8+sM7e+MT3nsnL7m3aa}RS zjCoA2)u5SnyCP05Zbn4%73d7Z7vsJ2_3N=h&ECV~K=LftLAHDQrRcqPfx`HY;2!Y@ zFnv6+rAs_{ORi9JzC!aH#(JIHbWz-lBb58$LJACvBTgvUk;!;QI6H@VwS6E**&Mfh zLFeH{8YWD?H7P<%PAtj^@hkj@kI;`SYO)E(f@mM>c6Yp{wK+SlCia&mA;d)qoBT_nel%~kQ3Mc zp=cWJX$s~8m$RW5#R+c4mxub3oUf*5SFB|+p36l;lBk#NG%w`-APY#lGaC6e9O;{| z@5WMA)V%mMJ9YGMVcTTTu41eu$R{Aj4}^&!Ao{_q3S(okIMqc&y-&wP zb5eXo#Q@wNhXWxEjiVEPeiG6?3>A9LMJp(fi$N_%+(=<2$T^T}Y`XX2BpP`CGa9O-JDFtSk zu9<4niOKVjt>6%6li^1uk010*ae}mk#Sk)Vjc}?;=f|&*x>)*ae^<#^c`xoPHb_4N z0D(s(#xdk*QzfDdA9cmd!$VKS*bqc4#dD1yk z;Za)?t++gSURP#I54}7hR#F$_(JWrS`6!@5M?&MsKik=wCOL5lal|%TSsOY3T()E( zK%9G1fED(HG~znLBx|qz*8n%IM-FCoSjV4XFl2QhKNCJO4_MSLzedmKqnKFPt_$?2 z`jH5NKcT64X?w?pO9i5IqV_pSHw^w)uHf4FkAT#`=RPs;T{($v){R6rsmOz2@x5GK zaUt+|xbiEa*7Ef$#Y7%q$xsjQC&SBRc`{uWsmGLJ`-o99Vhap)D&r5%@Y*Mo^TM?? zMjp}Ggk|J!0CyNRD(Ttkh{3 zy%z6qpHMn?eC?3tYfWn{JP)dGP}s*S|z!Z+rbY zAY?FTS}`7})+hu;xA-P8rLxvc9aj$W>stO|e^f*7BGm6zy~Iw%1^3|IY<{)LMIG(HD9Hgp>i ziK3LKI_z}?yehvD5zjst0MU(F1^f=N&_%jZKp#oC&_(3>MH#Mk4eunZf#bMDb)k7Z zEQ*hk128Vc?*{}czwqF3Y5QlZutC+`+H!(Ht&%Ddz+I%Q3E zycFlzQd*khBDTTzrv`;G4Ab}RR2WNqD@2>T1Y|G$)xKNvGBB}sw`buQfG-`}O~eP` zjub^KHj)Yyvi%!hcqLbBgwbpPcGMW{XrSh}n1YPT6L*W;P^c?j)PKq(xXm86D;a)8 zM@kf6N?aU%N7_I-JyEij3lUPX<>+u_&?U*oUh;sv`WJDoo=ao2(w>Os&M%KUze0i| z4?X>*nKbLDSOlT8viD4yO$etmw+L8GSkTZcJT3g~EG@pFm!vY211fwS-NxcI{WBu^ z53w7cjT=VxcMPnv{r-7_mnM}Qw#}jp3I`OK2~HeE zR=|>$1VSNCK}n-?R00wgeHS13n;KHxRhdm+EuUemX@k0rR8f*w_A!YSJuvN9TE84S zNlv9YN6gu2HQ%Wqb%i$ex0Ao!AI4eC_lr1KJj=p`43V^_4)~&DF7K-F4$vs-FIT;< zUI60n9f0&L4M3;|tI&jO!w0WFpl5DVsl6#ojvZtn!h^rgtL6ewZ3W`!B4oTYrx?6B z%`P6kBa3bBJG97c zL>;p%1x1m(dcw}_Z_@&0kk$qx=_x=+QTTV2j+eJ&DtA&;e4SN#OsH4IPM%>@zF7&r zl{dWQ1}s7Wdb)#F5Y?uCBUUcqbyu4kL`PY$%nP27In&v@PQ3C-XlHtvo){gYjD4-7U%bs zezIDCF-f5z+ulX!ChhR4wZbDk!y$f$X={#l@vH@1q_{8(lSL-iFkZqcVFe@j!M6n| zFIA&J5Pj#$3$!jSieN2Kn_BBFoFTzFEYaz-4SkYDJ{Udw{zIj}(QR-2`NMUahyM4l z`letj`;*!H|JM)0^gr2d*2YHW`m{j*9Krd=TR%F7e_sE)O6UKw&iL0S{z;4!GI#iQ zb$62ngqzl)^VbX&ruhT0reRc6tQ9dvzZ!!bV|Q)ZXq77n>X3qjv#p+K^(RvPMAz(7%rVk ziwrsueOD7BsUxs)PL3LGrksKJHxhrj+d-t#dM zi7E6eej&o8zugKrW{9ZH(`{El)$7#;5VeGEA{itEj9D!3Xnt!kasI6t5k*9@o-2)$ z7_;QH*g+wXWTqM*fo3g>L8Q5&X!JW|hCgfIyJZqKAm(9DGO>X6?hic-ns6|EY_Xn7 zU0}(VErxNCC!#_6+TvHkq^MLItritCsn&})GBNpcAZ{R)5Mt5@uT{sES66|)0z=rU zJV2qvuhvgQlwZ3pb{_X)M#!F5VhDk%gr^ZlZ6PFkUS(|_9sH?iPE?dxr!jz8X^bmM zYT&*#p}jM(7^L@=$nQM~Bb%#ZSG8OVfCX_FP?4}*3>{rQEJy5sYsO;Psxq4xSv-gw zw_Kcug9A-h6;pN+`BEEPJ8IJ4Ix$yDPyRb)&qF5dN{wQuPiKa_A0SH%x8j;LKkPv4 z)6f1s5h2C_+L@5hrp-SJg56Xct=;=%ZZ?8Pz}iO)?xa9u#EsBc>>y)1;+U&)>*T!( zzm&fRyxeX8qyCeNQ2Pwcfz>%b|?#omq&!-$snIxg7gP%9Ywi$ZF4y&@@TF3ynPsKfu2b&%ZUH84B6Y~Z!0cl8lRZpMFGRFJ8T65m%(7fvJH8}u*{))KC%vG1 z%2jCwfT|$RnenbElNV~w(4nf63rRHkSmtTaRNm`j4p`^*(hX<6@Zuisz8Mc5E?4<3 zy_f%SrhZu zOk)COcR&f126idY1$FVUb*;I^KxA&Km>gTMIC;0k&0`acNb>`9dma2^Jn4*?o64cM zYK@q?%)s*P`-kHlync)z0(oM+OjFaS&MOSp^#Cig7mBIlMBeJ}^j5GK>H1^Z({$H> z{GD}7o!*FzBpilldk}71ih2_tzhE@mMU<)`DykvZRu^b!SZCOdHmFkg_HVU;oK$0S zLg0_Ikn0^Qa-F?;w%Q`zdSgBlmGO#eb2kXnp~@cAoK7jV%g#WQsK_1^jc(Z$RbohA z>%gM%Z#kA+ygIm6vhgd0E{Vx6PM8|922N0;`EAP=un(f^(z3QKJ;l6Dy|3uLEu4^> z8Nx?X1($DDHt1s07_OX&X|F5pBE*!y4 zid9MbIHCRwOjg1{ehkJ5bHO@qL$n&g77<)^WmL&W62s8P#@<52D6hd|PQ2%{&Xqoc zpUmuzVys4GfN)y#=KxE{8&p-!jYx|2p)N9CF)!XPE4Kd8J@CX<^E9xLMf zw2YY7Hv2iT-QGvlYO5F0VS0_yg{}!F3?aEImlBoLqxkm|))>E{;}3&mam4jxEcK%q zl=~Ej{EodZe@i z3EkYwfe!xsSTc(VO5+WU<#rNvABE!#orwUSFh0`@B&U`S?n;;J$X+7sruNM zl#UhBvOQ{==lrt#F0yq*N$a#!DaWw;3ZB-1PLeW4xXWOn<#L;*SSi+T6NGZ*UG{11 zDp#$LA_fQ9p&fp7-i*i@r&7! z5@@J0K}*xJu4c7EN=OUyyy~fWIg;ylHksE4^8|D(ec$WvCfCROOOlaahViHFs+=-+ zY<(uDm78Vaee|As1EE#> z4=OQOq&B(BKP@#X(B=7e5b-{XP|5{Zw5=aL6=+nOF1j*m{C&o(RNh|J*p7JNl}F<3 zD8)GM6Ox1CtvQH0$8S;NGjAp5W3=NfGikaj(T#EypaD?5T`+LO)#g`~SRR;4KOxna zn!!z89n0ZKOm`q31?T?wjg!r<{=!5NYFc}PT;4BT0)Md0JRm*ASjrvD@g>jeg&8%l zTO!9AC~0cuP z3W~Pi1>|9n3N8YI4hz9c(4{{jbE_i3>?B0;7R8z$B{r&VT8CT9{$5mc1nVKq)_HB} zfzM)iRz>AdQU3>%&16&-*~oxc2sEV&gBbqWU}w5qkb2rrNgf!~3^TCJZDF)E`t?^r_ISgfpZEXrUoKHg6? zZe`X9wMg0&*8q;o9y7#9+z!)Fn$-TpoY;hpBw&h67n(f=n;#=OAG>A8U}JpbL6f1r zr7wCN)S|#erAg+sU->s_bu=IqwzI)*D-x8$^(on2swZX(S2od}3>X6L*I=3D3X@kLn=WI*zyfw;+hu?JHq-E)9)jqH1z^@ok)zldh3s zccN~xn=|XOa6@C&QQBQK6;-auP2I-Lw?l{@LumWbKGhikm^|q*8VTA~a;bHEIYSET zxoiYlL!okcm3aucdwR=m8uS-)hHsPyJ0_XrmnqI6uNL1|1JyV5gzppAp=aq#vM=pS za&v~n%O-1DzHLK4np_0h zrFL5I>Uc`}xce7kdt8pz#_LO&Zolr31c}yh?kk-WY|=;CVGRIgBcDw@q^){u@`hCZ zvE%3CCiKLF)uAzy5T`?`H?*vp!1JvJ`E(LR_E~x|C^zGaLG?jO;sNuKl%@koM(5HR zsZgjS_vHa;i-!j(ue-y8Pv3k;sBsvRZ);n!v&{ZP8;9=n>}iYho3y3UbZ2g9Vx4-{ zmsb{(RL2Dw1-)r-#u;*;?9tgr-n_c)So*@M<*K=btMTds3YYO1@1z1T9bkjH+%n<= zaMusHsqPI@^HI^~2hXx@RbBrGV&x5xvFumBR5d)P5#xbq*`>a8MPqeYre*2t0zz^674@`Z3?-e@1fvwXx!b$oUz0U>T8xo7Him>WW4RIZfe3Z0 zA=0h1?pA*Kd(qFRzCCoy_!Oh%j-=N^W)JS5@`f6v$6Z6`SmObdw6l-KIXux;fID0SvUQ^`Wh;$;FtB({VjQgDznFg37dE z96|C>zPLqbZoTlr#zwzmXcHDwvh5&IpCHMkl`~;WJ!;VfP?ZvL4FogAH$7V^$ZVFLLt zJxCJ>85vo{y=IJ?Y(}rsdPUh?vuiMRw-KELJOb?M2bI%*jzw|z_v!ff3~|=+j2r=e8(%=jJQ_X% zJMUuN>Wz`zBlVJaIi1FwQKu@u&1Clv$F$X!gkc5vZfqFNaJ=l}4$raaH@L#bG^0&7 z(*-4`w3ebI7gwc}qMVy1l~2Kx+=UJHE?Ubx41ZJ@Zb=r8GZZ90!A^ha&6Vr1%HtWn zHIp@hO!;I>l}-gNXLtM}&-MdIb_>D9K)N zKfVXAPS0zc&{i4B@_qRc*T2`u{ps-LB_~|Mwqf>p8`ZlSS_$b!K9f=bFlC-ts4y_PuGr!ZQF1w`1uJWIRrydU+5 z2cBnZ8AXfPy+?M`IjPGje-pW)SY`CwHz5-58QXCC< z*>|@+Ra=y^ZPB3h!z?uN(VV|}#4wD0Jr5hi+n-b`k1GXa||7o$%ohOUlxR7>&RQ$@>GF|ui@ z9x3mSFe`g0=ncj<)`?dxyHE6nXxia0NW;}UT1=h{7jC4&ez|2@lwG*Xlw|cFK39+& z+dsUPZ`gYOXD#WHeZ{8{;n%Nvx__@F{r3_*`A?z9*1?q4$k3YUr~So9`=5oN{~X(j zQk1m)8Qc2$ZGS$@CQX*tDIsnqnb6oDKxi+fOh)}ji5wp!>3&#;MDwFG69x?TJAi0! zO_EkCd=S9?h?QV)+y}!7CyKX09G;M$P zePau)wsfkNYTi6nkXKiosXinKQgReyErg@|hAS6q>ptv3`aT48rgW9zB9=KjapK!g zM+`b!Sg=SLxfq)hwOF1(zS$m?F(?=b+y5cat~m({WSznXfs%TENE0~09|%FRzsti^ z>+h1AWr20RKxv_KbmLuCRifGn*>7~sxDf&g9F%d=f?U%5K%on5z|!dPHFrY<50N^i{0)G6fpITLyzMJ#-=YJOlyI>inqMk zfVpWS22>+OiuN)sh|YS7dj_zpQqX$%18V=DJmq3Z!6?J zo6x+-7B712ae{I#N_IQ=>?JNJIP#q7hNBx9i1`obw?PTmhR)w6vzA(Vn_u1lX!c>Q z62sJvK0tx?x$O?nj!D8ir6^mzercS8OqO{1n4xrcqUI|>tgo_sw)%4&i5+9m7diH% zYsOmQ`^xkq8qwS)_9*ukzEN|4cf4}+kk4hEV_`}SLRa%{B$P;B5L)ThG%_NlU~7B9)Z- zxe4+qr3Qpgm*w`a%Mda7*7WeLqp7n9}DN7Ue@r`c(MZk=V zb_y~;1NrpVizN5eCPS2HWT37-@7(Y$>LEwAUPFci)0}Ln1cQ@xl!ivQ!8jt`j}Ieq81M)fgHl;Z#4$D8 zV9w61m$$Wr5nbOPC-8_q$x_WQOs;hd4O|2m{4|OP+M9$YIwDAfaj+ShK7& z9F?K%#{eMTTb2x)E^H``f}>=!Y>0nRst9S{x-QjM*OV;VZXQF428&juF({dVd`I>| z(w3uyafHySg-TN`wmwzD^$+O6msU&F%73`SWiG)&l4a&!5iIP#)un+f49YW&zp@cvMM97~8|nL=rVTaX9p{Ee^_YIS zw2LC;nGs(VN*^G}XU-_ewFrJ8wQ-aw&Y_+eIE(~_RWeIWoI5ncGRs$#Mm1Bk$I=9Z z@(F7e+{Q+lXC6nGd7xM$eUsxt4(&lKu+U3}G*h;#g_sBjsLDoyx8sDn%#%n(%X#gO zni=G5*@Pz06llY)Q7Zs-y)LB5;HG_(uTtcak;$lWuwdSh2H>Ot@If1^&EZ1w!w;|n z%Y)r>P>YYsWXV7xef-WRqu5b40S*Ke#LNt=_S-atbrwd-$Ty7Z9j*Wi^x!0oQfSkY z{c|*&YGNupaOe%&4*A6mf!TV`+D8rvzDJ?C>SnT^zDS9wTCG-RApK*ztZard%N#5C zG@2GgnbnxHb&=ab)}FjIuEg%V-3***q(_AiBsUJGA0L*BzltdJBAi)=&{Ou<-Ch>$ z%)za0FrB|j*zeYne}3XoYZAZg0$>dahBSxH_GD@rsc6H7Q>=K11#k;1%sS7h?}@~4 z0`dEInBH?_nWcyVBSb+m;+p=FYnq^)qqW24wU?(sfyV+0_@1U!o@#5)=Kp?D3ZYf8{ z-qVg!VKE=2xsj87sbIYcWSE6F4&5t|wn4VJMQ%HG=py0XrZ8OIvJ-r7NKMlGd|+L$ z3lC$Np(1Y^(`uoH93EHbL&-omq*)+MVJG;LT&b`C0(9$s1 zIf6fc!lP&9g;H8m2^zQQs9|+&{99q%TaB;5P`u|hIz->fi>~#BV@|rWp@O#z!@Gra zMT|hZs_X337%rHA9UV>!Ybf3@xufI3aFI`hNSx%E(#1II(?DYp8;ESHV{e>=w{uD3 z+#7)kf7npKiN<>}9ujlkfB0!ZNjC)r-`d2FOUvBwqZs0aw^bgXy}?}e;eZt$rnADf zCe5A}o{_zE<24}WV`&kv#Yq$h-UNkbAm`Je3m3p;#;5Tp-ylAb$sN`5`K>pVn57ER za>(DlzB8+1waPSo!IRbWP#mMr`QNox@E8NTWG{Sg?;5!dGs^;W~ z2~h5gwMyLBJtvtm*S2(AyQA~9YFhaX6oZTP76mWK!tDP7S9xdjf9bPv&IEq7O^P_( z71nwd?VWw`8h_HZz`7HV_e>j2B}9IYJnV(_Zl^VuQ@$He-h31CJn+JI;s6knOJJZg~P=7>kqR91D|t(b16T!2eC+`I7l-q^f3BjWO~9v1LkZ*0Vdro$uC> zdvs_qQQ@Yw&)Hu+y8HUB<-LgZ3zZm9rh~o1a)U$(Bp*bJh_>1ks?oi;*j|#aH}HkG zt6q2n8%^Af>Oq-p6+d{pF|=SuY`PQzeODQKWL+L4@Y_{x-$~$C_xpBqPhbA*!QF-6 zBVFdlQ@hywF{@j}35Wajw)c{4W(8z$W|G_Ma<@Ir7ZL#Lt9(2BvR{VC=uD^1pd)iW z-y-}0F_?1@K(52tF)b80wYt{tg43mi_%xdp~6V3I4DcW8gnVR-PZXI>BAY^ z7x<8ZFU=b(o`pjTp@g!QZodTioG4^odiZzx9{IN7hxI9KOh%il1sDo#cfm`?2TvWz zrPSpt0+rARh6f>)>_b2t-VGeiN97*gZ45#vBN@RTIL#Do3w-3;;it#kD|BHnv$F9O znQe4AQ|#4;0jcSo97>;PU$E^zfaf0xTF0_Jy0=gtP{?wMFtTV-UUAm9xyb5VCe@Cw zYTr27zeP1PrHqlwU~S z7y|nAm=Z5Sac(($+^Eb-udrl2e``2j=)%9pe1$kCqj54mmXcDiofLPC%!?bE3V6j; z4io;y2YkbpO}yk8jY^XBh#u0XLAAHHxL)2lH@Uh#zIb@Bxir1AJ^{3>nEu6+P;WhU zb-o_sSR|cA71&sAveb$p{-vj;@7J;K2I`yq#K>5~mSVF|Uk!ND6e z%|K1V)kT(wFDmHi$qaxy$tjqyuevVIPrp*_cjpvRz?WSPA8Nd7#eDV(Qn=jfT&xro z&0F#@~D;d=c{)Hv+A&~W<`!ONf}pf9pXf}`bGRyWYCZ?^qU zkge||mal7xeVbRC$Yw3xud{6vi`kxadN{{}^TaQj2A>Kuf~B0qW`g_VnYjn9F}Bg^ zQ&(VcA0giD+$eIhPDxPz?Hl(_8(gxC;l&qufu70Pb=#K5r{i6^?Zc}}*Vg98C$v^h z&v%_ybVm;Q9m-g-Jnqap^7xSWS&cc$sr~|C)DG?XH5M&8*YQBbG~;EB@YVM6zXk@CSg&Asv=$1+`&ieoxQolw&(Xxfwz zF0+W6`7u zb+d7kck(2+$by3>?2@+6edcd+m0WkQ^oVFhnDnO4;j-#&$3X7=h0va)eWxV_&46fU z2~Jnof^Ji?AD6i2ql_41oNP_L!Y`CMHY+BvT<2h?ns-oey+tV`3-2ePZ8UD;g7eL^@vG z@9u5Q|LEljw2JqV=Z{B8u31tS*VaXfMlK?=aGpRKG7Um=5V%VVvE0Fg6x#Nav0>6* zsGa;S0I1g2PoXV{tq|A_gSa2@LF_hjjGgy0WSecfsczB+5)7Bz;DZLCS&$wB`9HJbIp78O!s5o#UP0_{H0t11(`<8;+wV zP4Z+7I`zIOP+SVQ!`MMrd>O+gVu5!II7Zm9^QvOgmRr=;V(LEec#$pfhgz~u+Dd<_ zO_zg(;9;}iq&XQxiervZB$`&tGn^WboTuf;U@>3ie$p_`7t3&jb(qM80sq7b_=(^R z`eJ>Tif>~!h2#jfer+*DH~pM^Omez;_$uMCO_08LF;nF*T=0CJsvhIo2ML7`v7q{L*!Xr`k#a|?k)4i7&G`WtpIrV-^@4xhAM zE#JXK%W8Xqle$N|lE)C-eBCI|FxALT$2kGae7X6>2MiA`Z`E;^rcnjWW;V01dYdp|m`%j6wc2kM&3qS{VIco| zn?EbL4QeOu16N;&W_p3UR}qPCV`1hkhH~o`&&0 zJ6Ss>c}q`df6jSVMm~ROgv80k;0KTzAnap0H%Sm(R#Qb%S|hk0u%PA}n*|0LW#&>U zSOp8BjiSLqT^!Gl5ibL2faZRjFsyAa-hcps>1Ln%UxoWM=HZI7oS=2iEhL;LJVabR zB9&S;dUAuX`KD?=`~n&?Z2Z#=vY?j-=8-v-M@A}0S|vlIXPIfRdZ#%V;D>`5Y0)?l zdrG_xkq(6$PR3wBEh)tf7?QGW5%X!#Ub21U+>=Mw`r}_fbgR z(*;vo%3-HG3nRtLO%f|tXxyi3*?TV8=*1@cQr9fvGM}rZgm*Oh@{W4V;pAXEm6-$A zt#e=7s)voLl$T7;OyJ9%{08*ZE4=p&3Vko)7YH2z^cQE96I&AIII|PKI8*bMa?fC( zOjfLKX4|*3gB+p_&cwRWo&*)$Rj}a#@v1lY>h&{m+k#hC1YXOn!r4uvV_P6V3$0%3 z-7mFGKW)ybh(eQDXDB*}yLzG0{0Z3SN4Lk7q1B4<@-WU~jg;LU@El)$`JYGEMNZ%G z_%EEAUIe|M@-P41CpAqWUn0Cu9OE}WbMQz5w5*Yl!48>dmYi23I!(Pqj2DgWZjB(} z3YAcD`%mHmr~Tp!YOi?gRK&)sV^o1!JoWYS z@Ax@`p{I^BW+otspIY3H`x_o72ezc+E-sqcur$;5HskRtY5c-gDK!PXC%KBWy$k~q zruGO`#~KTLBn!i|YkfBDk>m2;lM>4!rg@a5^_9f2Kd-tmDoS2I;W5)5Stv>}6#JIK zSL-?>wO^6Hfsc(LqJK6H08#8jf_Ewft>%)!qYjffm*f_lJ69uwGgQ#;{+e5G#weH6 zrUyJwSXM08buCB%^Dnm*ebKv;wGDT7G;OIS9b8VjwEi}A#0BB1;I7%oKF9(u-DST% zg3R|H*_$E7Pq)D^sU zqfoJ@ad*eCKH^*(8fT(x1MIgfIs%09#nrJ`IIk}W^oRsoSY<8zgo+f^c~YJgEr6)v zt||akyl|t?TV_{0s-<}w429l6_8rN2>CSIMi`LDy@+vAv$NHD1avHXb6l zgz}D`1@`b4Bb8ps09Pcn`Gdyt_Xx8TefSJfH|)(7ROQa1{IEg(rN;j`|($v7|<`t7ImH=iEeBEd@V$fmcGm%$$^WeP%g&O1!)Fz zp>8Uz2VT05y_WHFA5BC+Ryqb1ePggqKvG+=VW`vSAWa$oB4=d`-xWT;DkDVJ`R{4q z=E>Lrcjk=gVT@fM&CqSkEg)~FyxuLr3O6e29Vl}sy~JH19do?1dIup=w1|1d4*s_r zEgOTAAEI1w;O)Ur=Ean|WA@P;-`MMxgezXnZ~U0`(rzGs4jYkAM7f|1j6$IvF)-UQS3aIsW_iB2 z4~E4v4+6_*Uh;dQGOKc1fasePDtQ6!?A@#kGuGbsI0&#EsDMyj;97qG7Kd2Xd-$q- z1B-p0P|=k6pw1L9c4})z9fdbyy*LGKTxqD=+`2i$Jpm?5Gu!3_g$Aei zMdlgnWXZuQkp&eBK1Y<2Ea=szj5{|@tqh331P1o|q+eue#;F!6nBqzH#QJg|mKc zox;54>WU{wvPQ`xk`*G$tCFN*cqfB@!#j4Iu`Az#AHE?eg%0lW1yHpEM_9j|y{k`S ze%h!$7ncl>%-oI2ukx=l|ABdT!-u(fNYBSs_>QXo~GjwVelOFauUEuEBdt4pamS$XZ+ir)(B-pK2lO`FxK=gl47 zElZT?pV{vjYFi(5c>5FAuGc&$MA_g(!^*L?0Ey{q->5kRfft=7o-;_~*+H|hF@ z#ORln8?~Qj@^Xsujf~hZWNHxFi!51#H@PeR%c2lgetf9cmKT?&dRq-lPA-PNrEJ(% z8^~v2gPwG-PsE0|-v05e?%SQV_=)h+mMmak3A^I^Z~DJYwd`_NrLv4!#}>aP#Hm+0 zNE?aR(IuJ(^11$c2i)p2Vn=w$Q>e2iwH0~36SWtZv&!~6`IXQmT7`iiS+unmv$Sw9 ziI0~U7K7$QX{6Gm@(Fg`+AEPGU`0L$Y8vtgVD7k-GBA;|w{XgtiZS9wKe5ErDKfc4 z*}OtyIWow}mucbFsr5bBKP)+ErcQewkS<+aLco;%31R|ma6rlcD;Tx&CFi;x1frfp z(w+EqT(Q;3gu9NkfeWF~@uqBD0M-<_&QG8HOJ`|`Qk^xMs)~qk5lb2jOO1@9#hV^O zl3ArjhY6r{4v~87 zJv)3@;G2Kk;%#@|mppSCTD>D^NzB%&NWiYRE&0_TWGlBBBDDn+8Di#54Fu9KBts7{ zyYEmwANJ$D{e~bKR?fO4`OKF;tsQoZ$gc6`q9(3Gg7`dJ+Wxdw4KvpJ4%!5zm>t6Um(ZkZ zLY@i5)YeL2RMy%-_+8=YyK!-j;TlZ4u4@{QJ-S!~7x2hbt#fs{ia6{^NQN)V1|?DS z)x}s`zi*S(=~|qh7Tg(s^m8K0D1H~g+RoB&ldtx;E2}owC@vw>;iVcMorgtw5iP_;0;K4l(S3{VuLQ2;@ zfMNLtVY#!9=Dr3BMj^?Tp5Ta|-R&E@86=orn?}E@qTX*^1C!>v=^!q#7Q5z) ziZ#%D2Zw+q!70HriJTU00gyP~W21l#O;rjA1Df^?ye9uHTYCX-OD|t_MOht06+;Xn zMd4J?t()OG77}8vA%&903n(d|mKZX(DK}tx3=9>plh>Bvckp+N6=JTabWiHi6VmiE zfN`W7$!XyIwAuu37rXlW3ZJ5fW9NW4gas9N+zS@YUO}=O?N)}AUcmb39$?fY>2YOP@_-s+-_o6vlV1n$q+kT z^v*##rKQFkS)+H|lM{JGF7_zq$hAn&~;16B<>qUz!b=w_9oW6|FA0c}qAtiQljtri9UJ;cld z=WD)2`XIZVOp7YO=e7_^Xaa@%8Xu}~i@G@r3#tE+HdT{Tm?LAKhVWy0yIe;W>yw#O_3Ot%85{no;GzPWb`)aDNMC!1+`{hmLCFOL9tAe5~(uXE~KPT_ff^7 zy7i$RVjImbdX|yH;agHvw6{3ET`64kzZ5%E_V=E zUDOr$TFu6sfG&AuY!P&JbC;1RJ)<~U#7~x()pwwuIz}<DK?u39b3wbuiUR)&)Ybx+BGZnuJ!dK@7AEJ>!dv4;hDpT40+_?Ig!1B zzJEZI?(~Be()Z$u@5|O{%t}TvZhCEGnkQL|$nLYNS;O8Zi6~j(Wzs3FNaTu22wtcg zho@q@7XG}LNGR|t{kbVg%^^||G;H%?DgiJtt8_chZU5sdemAQ)_9!GuYJOdh*X30? zR&&t`0xFLUt1OTtrQR>nm=(4PqmR^K+3^&9V&a(jK)}60i;*udHW-xEh|Ep@U>!;> z2!^o^Y_9VxQld>S8TkAuG*+0`6W%I4G*SjTzpOgwRTO)#%Z?H#PdM~O{0Z*h9!uGkpEcbi)ncvGxXE5QE<2#g4-2~JF~ zv;L;jaUdvx+Y9!QT{|bOsE}}LpZkF{6Tvp*Rp4f?mO_xoH(uG7KIfnoV5dnxnL0@{ zG;^>8DD&XE)AaTe_>Fe_MHh(c-G_A>Z^Tn?>KWMm`ZyOmpKsH_2vn>z?R!@C68=D; z;XvC5HLXv@0hiC()Gw1xW+=$C{_pwRUzf`k10J{v)*!{jT}I9^ey~6({kYqdTpGEQ z%aeE$9Vm)jeRtJUXS=kl>FTQT+`r{Qsd0ZZfyu3TB0+VWh1A_16bg!vQEp1CCyP*a z6kF@U>Uv_vZ0*a!_!(*j&Z9iH-*fwogSd`cdB4@}m@~7YyoXmbOF{C4%q$-~y2Jfm z79!u*mj;LH{b@;fVTl{KCR{eesD&bmMpEBGJTcw zDoW%Cc77FjBL^;QXH>Xr^VZdQt}{$c>A5n|5u@Xa({(T z33s5J_<~lM?EaENkqw*NjvRd-R9y`cyn}hI)e`eia!o7HyD}Tc#>~kS zy02wm4>2(eb{bQ~)kL8@U_2%46?-Fl;}UJ}+$v{d^-){qiHYxL7sSiq9ozfHsoJeH zoaT*kwtgJeU&P}_h!`VU^|*3#hl6{PYJ4^Rd#yXs=RG9VQ^0g?+yqEIN@~y=1wca| zq79kijaYKeOk3aRF5aS@?P3`VsXX|>$-&NT_1l8*_La3wGDjTZomA~^G|4<-t+6Ivf34&3 ztpd4+s$RG72G}E6C~kQIuP|@8{xkZ#fs|^FRK&MK(z1&rDtK@|*PtgL7zu;9^<31@ zAha3Qeq2uR*}&#!TkVBzYE36|&|j_j-W(ZK)(mz&3-^>qg!)3xaCg9rnp=k2E2Nvd zU!Ru?1@+ErRG)yDYmRm!dq{ASochBK85OtElD-;yx{WbH1@?t@FoublZZ0V$H2B*7 zeS2C+P-dwJb3Zh-1RAk8MgT}Rsk_m(X7*3IS!vB%+?^k(YgO}*uO+3?{5=Y0y?#cX zS|a7b;{0GvG;cdzEn24jv}f9q&&b_l{jGIU0sFZY@Rm^+0#oxmGL|=FLq{ul&kTqX z#~e0rmEgWD6l8GSFOlMS)-$~E>f{Aya(ct_;5Lg+^AslQom{>M&UYI2v4^>##jytR z#fH+iRroFTwOIG}w(e7RaV3SU)z6Q&i4ETjj%=l_zPsDveBe^zY^fwJ<0$n{w_Q+SM==ANEvYTfCt?sd42A zX?f2>)N4NQ*ULr1ihEm<=ZDzgILLBc-4$srIV>`gts`owh*~6I?3a#Q;8v>k0&N%> zq$sZeNvD0~8~On?BO>2QjMkR@%p6&TDyQ=di3{<<8bXZoriVLrXw#6ybT+C%j7ve3 zABn@SluXAgPm4JS)in(`J%0B$q?R*u;t@V!vLE4vr^_p0+dHBk$yw*FZty*(JTJh} zyD@HnzJ#S)f2m%WNVbmF^nTp|^(-?wE*SRT#q3ny zQ&91Qty+B_7A5sp;;5nqy5DeDf4BD}_~x%<>4$N33CreZ6kPxAm!QUv(vM3cX_y~< z4{!4X#475Nr2`-aodN|wJ5zcCl4qAWdU;`3=2&odHN@SuS-xl9q~K`i>f`@;TM$br zDt0{f-YB)%H2x8R{|go6Mz0obk9w^(t|3UPzoJ!yjTM>$U6&eB6pV(78N;a2qwt1- z&mh+!y@ZyllRpL}Gb0=F&JzUyp|~tTeXP4JfYFA~g3h#Qw)0P|oUwb%iMAgT2Rh>a z+6)1RRr(i}`OnY)ZZr5_n;~x2R+l=~js@(9U%Wm3ch52t8GS0FR&}XHtqoCdcFj*j z`{W#FR{Q0?rS;%6ltMOUcZ?~gTu+8#ouoU+N!IIqR) zaWf7-dWz%A?^DcPFM}5!Z;#`f>|=Xu%%P^EzZ1C&+5!iCG-~oO+OI1W8%0mCJg&Sx z2_;-#@ia7&hql=;;=cm6zNP1m->2|pXQ`#;T=6Dv;D8>sVSEtJ#5Dr(7B5{`G!$<3 zx98qvZ)&ii3feF<#m6W2xc>4byYd~x81B)1(r0Q>u7OYi(lppNvQJ4|#n;e` z7)`+L!9v2n2jO{mhIVMxKR9d32bvT7Dhc4Dl>!%NcK5*(#-QZCNU_zg6AZk~|5c|K zZ#Mv7Flk~YBkht&rwS;(y*|Tsbej7}9i@gb#3c|9C&U@=AACT&tPvRH;UP!|8sH4B zRaiD2sSFZJmGG9DQ_k+g*?kTo0}+m6!5q`@?2qcHgR7X&EP4)ywtXs&U6Es$2Sk{Z95hJSv^Kt@OM2yY@zBQ07cFv+J^&!2UV2V zn=A@sGi3z0A^|HSyB^B#AD0RdnDCYbo@6lkxNf0&e7Kc@nr^T*{(zt$%meT!tmZDl z185jjQp;FA=9MQ}%mZW?RLdCv(xYEa-T>Mgg!C6#fV@ZOoWNkcV>n1*+=Gcq2(v)CP1rKQe*en{dCICV*U{vRRaT)CZJoI)M z(9?!>3(&PPkpm zrQkWDsf+VS#o1)jNx}0&k6;34ab;k?(rgX)`=L}yR&YCh!x)4&m3++~9+S1CziNj& zTrZB#Z8S8pooZzX4Vt={45kBFDrH|o7lvK4(jkS9@TIWv@=+Z;2APk*75Skx4!(>u z7j|lV*KztY$5txTYETm@B5`Z@qD&(jY8mmA>4i zJ(8idO^X!ZOR+UrB@l9_8$94l#xS2D3{R<)@(N}pZo7(;s=a=OIw=Ni6FiQqxcyn% zTT`HKA``OQAQYXaLag=LN9r}!48)T?nyoj2LM@@O5(ro2r}w~CtsNx4%h5;2{yR z2A)dz2oX6U;BC(%inI-pX>S%(y^Q)z$3#rt2LdQ01C3mg0>E$Y(T#;%63knQIjnyx zPZ8a>lgFvea-}+u$A~Qi325ONr@<&Ce;%Qopi>U?r|fWOwCw(hOO~4Tdc49#CZnGE zJfzy(o(qFZ!(=5Y+s63jiXew@_``+V8K;yZd1BU&lfe6AoSLs;ZD4Z$8!D@vBmXY~ z^%*==R0l`?*lcS@hAnJzVUX+h+IyY?ZZ_wFH`lUfcYh@bkrVPW4-f63JR1UO$KrzjdWkcm)EhYvsdQN!X! zp8tHgarm}m(3Q5{;h6vM#)vEStO^+S*trnR2OlK)%(H3$*MPw?F4x}m*fzFXR--$aPM9>Ft`PJaR-><|=4o{P z5@3J}B7g@ZfQcU5#}^RiSR{Zmh+k&}WB?Boz0}Mn5x?$da>Z~`L&3U2@J#XrY-NM@ zbX(k4xUMnYE^}^dVFg+#W#p;&G%d(xD;$ao0I($Or<#*R zEMmv?@k|sfD`8jBIg(#FrK=Dp6~I$eryb`^;l)3%^I>No=UzgsyJ=tEg&d3U8b|=x z4n9(XB1vm(Ls{9*C5!f*0mk(3*M;gT*pE&aO0*eM^U`|q(j3$HkZvy)!n|+#kcbfX zayQ>?jSGdt77(?(M0rZwflS*_>Nba8)X5vi5f){h< ze&vyR2?KQowhpmvzv*DA=Pe?_%~Ii&)IgwX=)+~-pMd3U7oQo&>`GEm`xtC$(;V7X zSjcTSTWI>4&Lb|@y-s5CweO3W?|J5#dRo&nG^Kxh zj~+eJXm$^cFH58wjZ&wfIoH^^$Rzh#v`Nsjz}h6!hn!q4SR@+C9hnw)7)>hV@h7rG7igN-&a+7Bc#%)R znh&lq%kbKT@W>Q|S_V50HM6w!PV5c0fL0iPSZ;KOHWC@J3K{Y>ceQ0k%AIqDNQ?FE zF@lX+HCT5{%B~dsPzs(5{V>{yLRO-l_oz*0ExOVSQFyQWB3dO^Re+YLs&mk4&Y#j1 z$Ejcbx@?&ilLfCQjJUh^c|>oIp|Bj+vgk*|i3tlP!9~t-T)?MB#t(-BG)J62d)5al ziD7CaB-3}zaofQtF9VYf11jlTYMs19hZN*pHPK;C zT6=NDsFrHX1MWmnlW-L~Xt%-gkZrPvVEu-wEI2$O8M{_S(-X0|>Ez$gsXS6Ry?hEM zbbaUK8zGi#aL6T;2jf$Q|Gs9=&R$hI7%K!A3!@spA}7-jmsAdE0b2QS5(|>|CuS*t{8%Y3`_^Bz9>Kh5w-~e8wa`!-->aqe!)tA7 zqNb?TstcQ#tXPdMP}{WE_5fz;MYazrnQ6Ibwvwy^x7jH4dF=Hyg`=*%Faq^M0-@o& z&Dy8E%MNU^MIlT~cHg2@7Og-J{Xn`3ECq_N=zce1KwXRmuKxPEYW^r2(ChiY`^Q`_ zcX~F!9TEUw#_hkBvj0BUBl@38S!H*-|E2mEl&qt!yo?gu13Yrm;73CV`9)v`j4tB; zW2WkW4NK2CAw7)&Mz$t9WY9Zn5DXiTp1wp}%ur?Cv^Z2)%n)TRQ&Ht6K4>w&^36Qy zOy{u~vDJ*$bur(xY_(7>7c16%{JFID zt?nsq;N@t*&&c%M&4C!jwk09V%7*ykLwN3wsrA4EoJO?YP}kS=m-5use0O6gOsDF5#~zcWt=RTGQMVjgTcIg)?^ zu;#`MwzZ6CIa!xYQ}$PFN~R^}Z|HJ>Wj*3rbSdaWmlDRZ4;Nr(8n~E;CQ(W=v1CYU zp)S98X|IpGY-bD5WWykEDo|B}CcsyHAm=P8*~0ywDbCorw^0~jrGljmf9F*KmsVPko|%_ zOt}{XfMTotU=|~=>&Y0=tI2F;&*eX+?CL?!ibMqK4rXNLd;#1KQb3+6)F!!BE0OBn z0-o}(_I{~%NMMvW8JG{SFq%PI2|y4i`QT+9Ehtf-u2=P)7BIRfG7ynvCj?Dr@SqpD z`!K<7&qJWFW$MYK4U@a-H5jZ(-za{ScVy;~){pOIo32=yh^fkTM9jt0%UlPKX+DR1 zbauk8!)6s=6Z}1NGji)#(BNGH(Y`V}so{8R@;0EICT7)rDIn1my%4}Ez+Y8CGTBf* zLOX$w*@X&Tc>`dRv5;O$JB5&$d;N`8RP1I!)_8Dl&Ak*TT$6I;+mIoz^gUE|nyaRx zPcY>TdzbIsy&A?jC6a5=Y0>>y56QjmC>{J=lY?)UJMb+xLVi$`ZgINbZ3*9Ty&{J? z`G%N{(jnPNZH8}ky(B2@((?)1rXFn-Igr|_Pbf{X{y2`EK^A;yt@*!~PZ)}cZXB}C zftC|F40HY88nI1ng7qLLN=GU+TP`C~gPgx5^;Nqvi@ez8(pZ}sr(DVA<2zRo+G8y~ z%A^_TS-SbspoX7IM#~;ek#3!S+8z|$mb^}C@?jr-$TDhIg+q+DAIi~p;aZwR$l`X^ z(`Kw{se`%SmY?_Aa>|Wkqr*rcVEafONA*|2n%na@a2rRP_tXwtNfmbWN^QdRbVU}6 z8&EdNMIM}H@8u`g+wIvN&V7J|mzrEnHj8UT&LOHbOFmT{O<+x&njuYBi;Z2xbq&`=MYt7P1$oEn&%o-$4$C39t!6qRVhunvmP?%kxLpy^#b0gGqOoP z!WVW}U_LSn!sTBot@Jj%j^M`g!eG36(kPypcO=JUACrySV^yUz799}w;3AYgC*OCA z&!OZ-v)fxS9J0POr!D0(e!t%UxvkIDCi0)&j-j7-qAhTG4l}poo;?@NMY9VDDa9H$ z`LiNNhp^1tYEh26H3gR`hw-jxO%g4T%{vodjOjMYZ%n!^dP^*=EN;|6Ed$1aaAkU@n3OLrI%%ugf7 z9!e9CYN^r(r4iQ0o5RgFDAHdVzSUR$_J}s`sWibfz)WJ122X{R98EMC8wrv|lIGAD zPc-GEMq~Kdi7VHhcmSGTK13`=OVdqumad`iqD*1z8hW5)?W|H1cfRR2< z-HIJg+cJ9d)9!maITJ>n$9 z%Cwni$wx@*QnwYB+`4igjMlnxAuOe1<(Q9)wkg{Wb_yx%f>HQoKv_V5*nzx zNH0+o!9)CakT;$ZZ8}c51D&AE-u{npyLbY%6N7-_IiXzr$e=>}AgwOG#@?PtJSSRB zEVc73<|rdtO~OJ4yU^Uu9`>jMnr(cg-7V{=B-(X+rNb@ns0A8cEOUFSWc|<}vqVA! zoJ6|LhLefDc8_OGr=RnKCvX=l)+~R^qv4A1vn04Qw;#af+IuMMZ>Z=wsV~aSt>apl z>s<=zeRGe)_8W}$TUYJ?-``wcLU_p}Ckz$_m{BX{R)ut9HV>E``WT)wrjc{QM!3L3 z<2M+lKPqY=y!bVLZ8!er-a-HkZJ&(a;{C$-E{-S>*JJkWE2;GT{EuXf52(1S{bQM} z4f8L_`rp(I5&u0|J3EU(s37AMHuVAw&&|G>=0d+kXpLfs4Qc=RX3~^}VF;b~58jxY z$pAHG6!T@~*B3{L<5b$b?RF0~U~2zw2%Hg)5l5%|Ui#|zF3FviqfZldwb38LT`5hn zA1u-9C`m|})qLII8%?cE}5-1RxG$*nwRm+cQBo=w1db_&6$I zr~bND3p6F{^d(-YYuFMQ%sM>rlX@M}s6ZfvK2##J^q3#F$MuGemIT6x2qj=s&~>%* zGceP`4wod}>ylJiWbA1p?I%~4K}<@G0_UJjkGl7p$wQ#E*KYPtrlHb4Y&B83&yajm zZJ%QJP14kn5xgdjI}C$`TCwsL=_svEFm9wDC=s0@I)cE}I=Iu!v~F<`(rrS{`#^*t z^*MrCD>FMznM?alcV$<)26JXu@_=F$N!yk znZf!#-=b#7r&t#G%qq00oVd;t9QW;-<5x3mpGqx8C^H#)IBDP7&&;L7+%$p1Ja4!q zo+cV}VeRj8a026Yk8yEgc}kQCcXag?==fUYI>Oh?&cCucMDSM-s|KFa8k>~5**aD}bajR%Z}tVPxqRns*fdTzu4`^RH#9 zgGp(oKg}73k(LnwM;*$R=;7RxY51{|3O#c(5iaNIQ^vFEA^X*W6Nqs|VZ2HJ% zWy0uKa#=IQeTCwHxU4FBvof7aN|*%$A~!WVpv1T@fFJ0@UjTb+S@2k93yN1=_ET-g zxWn5%KCh7az|US2d4G37OY?{9^w9e0Wri|F#ccwS3hM2KQw3p2MUy?-(V^x!-M&Q` z7n+r;CzOJ#T)c?4!s(G-={Lx%crQe5p}m(pv$|tAk2pR6j^XK<QQd@gx(q=_0H=kaIU$S@uS}K(8+Tbg+yE7xf>)GK0B3)1ovqCHBAdRD1TO3A!a?boOFf|BP{tm?H%Z^2pJoYY6)O>dUZO?9{VL8| z1IMd}5tCzHYr=GjCORgQxV+7dN<|fdNkR2d>fOq-3`1VI6nmKfO-cDnQE-A zCb10_ug#-BiAw zOFV}PvzlliTY>i5OENJ}`*Abk zgJz>}s(7Kke$`0*BO}}MLQ5CfHxq6C%!CktYMqVe)&#^7RX&zQ%KyaE3HT2|7vV)s@q{iX+K@w@8=~*$|V+ z*(nk74f-h$lf;ib6guke)dZ`Uvg}41tWHuQGerXAdX0@axrC9t#(>tz@xYE`W*W`Pp{}d5eoBvj8OkasAKqFs8f{r zhdPY+Ji9%8F^pewjiMPAKE*F2OxbGrie$-TZT7}VZ$n!owdkk|7=K?-GI($w06s{D zjwpF8f)UBBG@Tx{dlMI*ueUGYUQ%4HRH4D(!~Af}T?t+2E_Tf4{J4z=Qc8&~oT6|e zdWHIp>4-Y{ssg!dlDy_5qc9Jd6^oFPy?`6VIG8S>Arm3KU#6k+k6hm1&f0amt*wwL z^tOZ;FSqu!Vs?;mgRd_aGghvDBLKFCxD55xui@H7ybo15F!{IVUfX$|<)gf&aN&1@ol)S1`!h zd;gTt8`PU2^t^K!9q=rFw7bF^w+yS*{5SP)^RerOs`{p%MOCk`b*7E2)o(s+s202R zfd)Hp*C<@5=amX{+cci4)9$clfAAyK0^GY21xNM4x9osq*yU13Y1?hU%Kf+yh(v96-my}6-1ADb zfmwZ`v++lLGi5|8tc0?wnGr8JQ(%KLa56mNy!tXlV_hUxtV_dN1&}at&puwmd0i^2 zm^jm>#S6F6-*Z97hLF4lyNs9g8$3oCNTdbQkVG8(sn*PNYR>jawT#97Z)cbWU$$Lr zTp*@m!_*G#Rf?}R>?dOv{7y*G6OGQq@zEoR-guxm^J*JA^Y+Ry7A?Pw3tAd*cSZK) zbg3iRhb@B)+;lJAuj(Qr521$aR*QV+AxMZ?p@XCq%&GZqr{~4Q+L%FX_23%BuPCSt zuYNa7Td8FK&0v;vn(B9_lF=Im;geQMyCf!@Hop3uR{P<5%1uVKH7F$Lo%y-q5lz*Z z{j|~%X%263QPn39X6<}9O6U#fO$)#LQcVx^fXH<5zI6QA9=%F9jC~~b0lUtqbxgG} z54E8N>bk%DysZt+X4f!syn~7w?t{HyQlsn^aj}|xg}j<<*Al9^z~e)00rY&Uzbv#_ zT%n?YkyEvX@sFgYwLU&I`vaHo{}C?#Ck4=dqUE2_W!;P2Svmvtub2c>qu&|~;Y_j; z@8u;^3!Pkce+$Ns5+aEWa~SFQaJ^Wu}ZnV!1cnQXcBe7(Ma z^r7HnCUW(2&GG^n`#s{pF8%nnp2U7vWR25XTuY<{e`<$c;7bf^v&L+9qe35M(Ze1x z1>>x9?Ur=n+_IbDA+W-7{unh)Cq&OPDfCqW&I_WT3hmc40;YTjV)k3%8?lYP$CcZ5 z*esu>r}lUHr3ZmhGcJ5J-a(oD&~nj2go4{QojWD?hkIGTOeHrbWrEMY5icIl!{0~$ zKu&oYQ0pG;cZj_Tr-l`{t( z*Wr<1OdbWr78v#Fx7kzAeTJH*jX0dioHt|hP+I=+C=F#hBna4eZ)rhYa6?NRtfJ0>3?rZ|L zI!abKeLhOcb&)9EdV@F{i`FXkG5P+{suYSkjv#~Dz=P=O_w{x91xNj0 ziTdM6K;t*L1DMh%1zB)0mYc5hJs7SxZSIsM;C8EH<2Nu39mHc?QcfhP_G;~ z#Wf9R4dv)Hzl=#Kw@VTdyBR9VRz>-@t8=UN!>pP$pXS5p{*4~p zJz97*&HPi?sU|Apw)A0Woo>>12PwW4%Zay6zCq{W3X%%~`0A$MzLSROx5kirglM`ZyD(6EC-(QJqAmLna}+j@a-nz<sR~Y}YvflmRt= z31)el7h6_leC8NqETs`{n8Jg1Vt>Ev_n~JE=dXT#foyhT_e7_&NI)t4VRxf|3iU`5 z$3IERvytI|5egHRAx5kX=av>FJs7)x?JJ9LJqZK-40chA-1?0R-70rc`5HGxkQk~n z2rBTYPrS2FzNhOv%*^YUN|>)1k_UQ5bZZFjyUlbZ_cvjMYi&H4!SO6PnV_bt?=yJs z4TaXH0AVQDG(>(W6LBgrS*bUIUU7)}$|k^!m(bMmAucYQtq=Vu!Y&egxrd~xK4Q2b z}Q@Jm*yC1&*rAx&P3*zBqKkpuTDL+W#2J*!~M;->gXpm9O8v-E31> zD8c{?t=zS&mik83n9wj$4&9g1YH#0_ry+7(ua1Z9RplM5>nxJ=_Y26o(h$#lh(FVc zXBroa_jKoRc4xQW&o3xL7zNvjp8rHyeBp;U1ExI$NbMr#D5Ld-OnS&>JgOFrRH&|c ztUxwY$h$pe=p9+V{iML*wI|7*Q}=A~O3U`cJJlVpLmM6r*o1Zs-iCj_Vyp1E(A?}2lBCXa6pQZkhhY;>+$WaPaeH;X_^F{k5(@8*Wpqj z8JhV}4Odi3TptgLOVSHA24k3*FU%t)d_p+d=<-WG&Tb3_#x3NG6{YbRHj;J5x{NQB zEKX9G!LTp4zDDa$jZ4j+Z(GhMUNQeer^uQDM!5sKjaja%y{#aI@T`J*-M^qE`dWX% z&si=AD7*NE-!Wbdb*0%W676E4Fh<8p4aG1hP0*ejdzkSNnOVIgM+UHsU|0rI!~$&$ z_0B8zkg|%4d6(K?`*0i)>}<<7ihv*GZ2mkz{FBVu_t%1<|3_iQ{$DWre~P)^Y2>K? z(5)oFeQ=~)x>VItH4!qI5Pa>!A)|d;ZiA|gi$|Kg&=EOUk?hX zU^{__MVUUpJRdqmK37y(=Nab}EblZo;7<_okz5jOk`dC`&my=WWQ;k;@1@)} zr8NZCkK%pTOM57o(zH_TmIyK7M+KU0|D$k7Wlu)Zh2MJXS>Ud+x)ducXZ9V08!xGb z?GlVn`9K(zEq;{Sg^68Jv7$?T`8b}$C*dp0*T)2KY)@J$Hs}>o((aI+XqQ7Ied_a_ z6Xh}coESy;53xlhu3d((8hWj~S#c`A#^d^_{5a#TS8psiWebUu;0suv4!ZuREfjnE z=2z4~{SjseLw-7HRNc1}tjT0dy45F45G5!79klpGW0BDSK%o;hNl9PxjIiOI^E%~= z#eNy$^nwTB;*UJ9;{N9CV~8A{42;00yXeCyF`ncOCRF<^fv=vQI!9yoTK0>n82tV3 z5#NI&l+;**M_3meVp!vvqn6_>k=|N5W*f(psbUqlr%U*zws5=$^COC}FIsy7->+l# zgPWWNU7esl&!Wmdc&bLw!uscN{GTIR9RG@% zEGMF}-U77<5%+AA-lx7|2Y^ud3jFHV@GTpHAn?86z}nvgi>8w^OW17vPFtKe8w5OY__&xH>cOM=i}Gmo#j=-AitU?R4| z0Nd8_8j}V27ajd8rxKNFmFoGjv_>e8C7QwF5YA#XOK$UAmIH>KHQl?PhOqN^M4@Py z%pq3j!!$OMhVo*{!*GR|38^TUdwN&y&ztOzh)fa1Av%&3BPT7{v;8BdxzL!%_qNWIW)u&SvGykF-0M53=zp zB{Cu|)<|nv!|(g&&_YUGpLwm#)=Tw{eFi&D9xIl@YfSx*lSI@o{)8sZf2TEb|583S|tnZ$2RdD3!~H^x`p9a`EsN)}#sz^+2VbG)m8x#Kc0SDkKgjRxb@zF10u>7nfEq34h-OQOSjbq^V^TJg zCTeGA_hW{kowS|`q#C&jXsIh!LuamnG+l$PK?iQE7cuVvU#vX3PC$PrJ%Zk&FQ9ZZ zPlj#DJl;Nm+3B2l1&(U9%K4o6^lb9pxbqATMuM7`g1}g>6?ZBwDbrFjk+6Cj!=Vi5 zS3!ZKBzKgmm`YIkwd7TY{e5DKFvv3&GsY(7gghfWp3@FBQVM-aTi%btU6}3LUtvG& zh(d*O^eax^h&S#%P{T(Aqm3TF`r^l=xj}GXi?p;GZo^)wfY=tw_qj3c;~qh=6BGurX)E!Ka^HZar|+Mm**h)+3X6mh!V!^bI%W)N^5Cu4`vb)R=}Zgw=27XPCA>P;F`~eB%_8WlK~FM2 zLYv%$Tq!>rr>Aqu`RYL=YSz?K@&Ic5iwuv_Qf4dVKyi|Ln15i1dd53L{S8B*|1lVH z{TB>%{?WxgL-d;}#jN9H@^pjHwW(<}z@%Y8iP))rkC5Fuweuwn)~#P;W50`aodp`R z&TcU@=Zba(rJFvhCk*+E)C4kHOAG46V?6=%d`h2FxVVB;PH^o3zM;U0mAB(Va@}WQUmI>a{4G zKpW!`MR%qi$_+bL^zAc=8X~3GWA3*dgNwrP16uL5W<-aaODVxIavI+cAj4U; z$As_;Z+8QqcQNob)}DG+mlR-pNQVFkKM$JgtHuP8rPIO%2Khuno28&ha7ZR@Qv`AR zO6M7GBFW~6rRiH|qj7s89j`W9dX}43|2m$k;cZC&5T?n`7Q)a1Ij#1toHwra$tWG1 zGsBt=B|`*>P1VU$3rN?E)4^@Si$qKye-{9o`dI1!DnbZb&lbuk+%ZHBW5o^T3rQ^gf^!Ht1BqXQ zr@_9xpcDnh#ypaJ{PVyzdfAqU?*(d2niTDCK#VHs>Md5*HDd8I&Fr5O-{gpF;>WB+ z;}a3+&CFuK1Xu)Yk*GS!Sx$nF@ElXf8W9SR{$hyoc~1C!19~Ax|lL$2{5vnAr%CUj)P$b z6{dtUv=;@DqSQ;M1H~avBx^r4$9Vb`j!G$6tEp5;xzuhssDlPbj@Hw@jd*oG8GapQ zxSTfFcQF^Qw%WY-eB!m3>NxgfIOeGtiL;cRJY6*P)^%eE{Q7=IP{CQa`N?WgDDAwPT0k;WU)Z} zR2o#|QZqdwt;TvED4v;S zCT5fj`Yqk}!+aX0Eclyl7lUw+ih8pPbM=PRXQl+nPc1h)q>51}__0aR6rd`-85KoF z6_+9agA|EAQS#TnYl$UTYE6Zm9hPfBCggvTbHycnzKpJKxA6Q3kjK+IDb}B_!#sao zipIgK@G{l(I#gdV^PZXK%->4{jwU;@^{oi;QzlC*1A6GC;OO;_@qe#%MgnQZ{7&wwi#KOBKAyKea>rA-A4)y(^!tMqdXyA{l5jLAHe);Qm=c~TH%J0S z!-bR%xzo`9ln_&&aCuMLMpkky4u@f>MlF&m>JT%` zRcF$>rpxW*NNAOQi^PJK=tEzYz*s_Zw@I?V{HK6v%>X?@j(kFvU5v{?gv7m)A@M?5 zZY^{XMqny?ZJ#?Yy=m0Q23;~Q@|z6CX2LI z_7Xgn%JhWzYQtEF^inc=;_(iE=4#888@~r0z+);8#y~A)k5yNZEn5gTnn)FmR*6&*L0WIz>{2+t>KRxx3_Q{v)rL{%MclV z);l_KjBqTZs%Ul!x;Jkk@fX7MdFcUp`VXt>tWHkvnuF^z)1}?n-8uUhwq=uzvQJyN zk-*)i-5^_@w1FDI`T0VJYSK076vMtE`0UhvY|}d)nMzcR(YL#cV@}exd#K4l46L5V zGqbCvmO=IfuPN75InA2}ryLIzrDb<&ug5pxYpKy|Dt7&iYok*xeol6u1dTSpC5x*p zS!(N~U%qG6CDE{*0 zp+JnmK1Y-wSVGHab}A1^2=GyhD02r*-o*H$@dN76WwY+sbIq$jLvYW_?du8opqlrt z|507_Z$Qz|41Dt&b+Yufn(8u}8g)jh(dUAUMcWyyUDJ7{aPybe^N^l<#i8mg?pI7S z5hc7yBV#(6i6v^vr8LguEfY>52sFo{b;oG7Lb;1xd?yI5$)PYwgzKOQk0Si^8>L2& z2Sx>K`c|{9_p@@Nv%7)^#ukcP%*f`1%56%PXWcVrs-MTG0-*o zEC#&am`=|X?-ME$Dm;G`>WwUZ9&t#2uSM(GE-yT~6D6+CltIrf?*wtBBV#@~W4c5` zSeYYZHCo&ViPk`^Bcq)nea7%Dv3XzbG&ZUaQEZ}ECOpJ6X41PZ{&55+0eqKj6535L zuRhhDG3SdJiDHk&okwHX`0mbf^c`?l{IE^PCwfv1M<`!jO@as|>eHbkBkI$!VMw1f z{H&tpI1v^!4lcL+&WaQ;#5B^Fvqx}`@e_3(Z|JhHXVPYi&g@#cPEYsBXU6zD{t}Cq zU0*DHo{1!=lccJV^n#H%>wbsjpjmA)kzXi~oAeC9jRekDkN}Fxdv`_LUxv=RKOh2G z;0*Wh@a-NWV&j8U7_qIK`+HUx)9oI!gX8pD@s^ffVE>SbUOCCAyYJ~hhVRYP|Mzh5 z|2_^w`F}+w{>3Azv^+gj4?F$3)4$uq< zk)|_F45Xuo(&;75l~;Azp@CI7m)U9CQq3ha5F6UrtH3XUB1$;Gw(m1KHjoDuMQH2fhIJ>G5KHIEWv>xsWWpX-IgfUoAuYW%%=ZbyD6W( z4#p(PG(K_z{5;v47oAaoqiN8$XDWBrS1^Sjjqs2_$QV&4>}@jCu2qWUd|3v~xG~hdb+VybGu>=dsIO_G za$ytQl5oO|Bl-@AIia<;4y6N}m>hlwU>4Jy@TKgpv_=W9dN^@UAqDFGa)HYXP0!C; zY#nlLyE|x{1<7*Q0q%t@WR~)7kAXp>_p?h?YuZ>df%!WKUalzT9ExMAQ(t>ZTZRGc zSrxz?Uv0s|ebhGlU)^x;Suv0A{FzyvuTT536g^J~tue+6a_>Y% zvaGa&%tO}}6u8@+$ysvXfp148%7M|B{BUu}Lu}b2JepX6S{?!F^F9@F*2ta4r7|p> z5>^JF|5bs6-V_N(D^wtCm6~jjKDkeL}W zd=AHIbg0N3E10XY)?T?`ELB(T;bsC^fkvp`veg4;DECrjs!YT^*R^Q33E;`<5_+&> zt3f{>89qC$WfA2oDR=511_Qx>7VGN)SIU0e12VgZ1GgWLYY4NNF}2{%qE{f{wq>b! zLGAY7wsY4$9c-Q2ssapYHFa)YIVrEof@aK+&sq``Tq^#pLSt&OT+eeHJ%wo~U2<~8 zl9UPtpRWe=*+z!7OJz1L3W95KSIh%MeLB24+JpAYk#bN72B&E!nPwvGXknXaHQYU> z!a{4Zgq_n1cQ(v-03Aw13GS7di3X|2IcRLYB9IQmdgR`bGo6DyI!>X&Qfp&8w?v-- z<(y!UJ|zLoMyWyh&2$DM2~@yzDf-tvdfZp+jtE<2jJ`gX&6-(NKFr@UAvCGYIvwz& zxRI1?@z5KQ?&mKEAlJR#Xynn5K0~!UcH9gH%zMXHm2S~33b-B;pvfzCJ3zdW8us;b zolR+8ZnHZ%zT0b&p{~5PJ0Ejx)YHH0tNS{}j&zjey_cmcBq*svDXD|=>q<$LK9xY; zNY5W^>C?wFt(fe(2aZAZ=PC^)(^LsqZ-^e4ESOiLWD9=wH%a>p^Z*`CJ$yE za0N3S&e-t?EZAd>a~g1MK)r>$`+AIgel_l!YBf&;|5cN-$TuQJt;hl4B+lwE*exz6 zh7^M?F9>{O&0l3uSUo-XZOc!FS76!Oj>^r?R<7X%Op337%^6W*x)n|ao!S{fbn9`e z?ua-Hm)SFq+MiN8HpK&Qrn0)96EK%DoE}jhX33l{d1*4g>*V8G#ci#;s&z?8g#e5=Y1P(5fZv+fK!+LYKXhV@QwYb~!4v0Kd>395Ti z9LjN&FBZz%xRaK_tPNk^L|Ae1_pJODtS&6dX9|6Y(W*p6XEZrbUCEl!;@Ersp<94q z2aDt-nhPjSq=mx5LFbGKvhdOl@LTuZuHPb(*f*=o9#Hv2XA1!`x_7*sotV!P8&GJQpdvDW$2iF>v|@IE+AJI5F(ZPcYme4r4f2!f_ik1JG{EVP z&akXrOn^)z{&a-3&nrnrAKKS2xZE60B(Cb`7dD6s$@&uCYlj|UKI0{<9J2gt#QZvDm-cdMky0@H}qwY;2z#P09;Yz@83B}jWE zcUT>q2@7wR%O}eU@p-29$|t~3lk-(3m-DZ(tr2Ayy~S+$ja=eM`q)D%s*sZv%PI6O zUXB44ES_b_uY<#oJgHF=^b+YXJgTVE7hHaXV2i(p{TMHk-UQnv`;i{@9$zxNg||}! z&&;jRtqNP-8y0%IqmfZ>2YNux;Ofq42d`#+d|(2s{1}U{f`7{J1W(7>{A8gEF%}o_ zdl!C3n4~#@qOqYVh{YJD_^9LeifviQ$;oa0py&G4st^}QV6dw#^4WuZ*OBeOVTk;j z35<>tqpBs&VnC)QM!Pa~o;ln7R`2wMx(mF;!5EHDvhvS9PJ<(|RhLxc#|!%I5aR_{ zpO3T9p|p&T(JO^)6pDmW3TBTPO>91qk`5r4-5;!C0uN5;XP_Ud28!$CuNktB768K4 zvB@bgUGd+y+I%8JxU}Z${y`<*lIo{?UTjwP3lVp{)pEZ|t>w_kT1D2GSoE$YtbkTGBtGZnJ_S zH4ZiHSBP>mx*C8Pv>u!^c#eGB3??{q^W5nEHoQhjV|rU%sY;vKaI}*?>+F4$72y%} zYOI6!LHGwl4ErN0_Rvko8_K3#rj0%(Uz%6XF@Eu=V?8RPJ8z0ExnrAyHk9pN_fy3- z7fx1+8$J$JBG?@<4%R~t-0fim%->cg{RmP>g(0!K60oWAd<1>5wY3;QE;9Sa@3(!IMWIh< z`KgX?!lMDd6^yL~xFKdb=ekF>bH&)MRB^ZebbX&$sGa|Ifbqk23K{Ik+KpruTh}>SPm0AJCa$!1L0G;Sdh~Oo$9;J<1 zO1=o#!xfH5Kj(mpgrx0s3A>5P5$40fNw}8Mc?zEZ*5%g+l@Z#2#t%lU?cJSfo(K@A z5T}nH{9cv;>sckSBK4e`~U1b~35f7dKwVO3AB=;JX z%~Ael6pb6~`Y5wza0d&auD~Ks44FIolx;rRR`=U;{fpjqs$D)0Y@mEWc^K=sR9*xiRO`$s4&>eucd*s&8{a2nzo{xIb~(fOso0UdW>l zekwX4AJY1Yxv;lJ8<%;Z9hccG5{4=a*dd$IZ=4h3HgSKBe_$qgDuWU+2IZR5&(@w^ zQ{tJv1`iuSm!yVrp#?yM>_pgEtDJ6}L-E2UX4fiGuZdC(-#-RMk z*Bgxfy#4S7(y*OzNOM9&H7&(9ZQQ5yf}D9uU}B;a8ZzuMBlePyW8Nlt_|n;F3%HHP(i>V!(@}Y2g@@l!#G^Q-KxpO8G=6&z3)lZX?K5^maTMabtv_HO zN#yWzaJw`?p?W&|d(d!%?qbuPhCc+T=_sOY|E*Sro%(1r^Ap&7Nlmg>W1`|whN3c! zuCvG(AizI)XMi&wV>22ecnOU4q{N545_?*^Qy(_;i;1or<#Od0VTt$lj*J@`Fz2?( z(JzqtVMo+uw))d&xGi<}v0ymL!&f-3&kWbC=9Msg!^NLCFkGK0GwI`&W-!Bh=KMUI zsMlm$z7r<5lW+}=V6|T-@FQfFecON03FUDA><~Pdz;=2hAG1gIpV2qVANaL-uVl2} zD(o@&u1Lt&@pu%tk_dl^hEL&)SClR@?UTZiFA62>; zzi$oqYzr0v$9dhLB^b57LlNPTtmkHiw==oi*>ecZ_UKbsj@xyQ?0DXXh)ZvS&rr5^WofGMs>gXKg zQ^ctGiNQa09`WJ|e8Tqzm<0B}tMl;x+d5C!$id%=*^ZAF@hS#Fh$<0;=sb7vu`*jwz<6Dwo}IhKq6m;|S0gxLIsQ!Z{xjXla`Sxs^%4;lVl^iN?~HwL zmnhywyfwi)DhiXn=jH0;Hb170dbZj^2Wl>Qp!SDrraYx}lSTrASZ&aywON^R6SGc4L&>4f zpz;AUg;%Om2Hl~ewd7IclCYc2B6UXkBCs`fVv%ieO(xZG(RR=R| zz&ohlS3D{rZc#Yl-htX#W=DF>JV+JA<5rz|j`;5*ObjQxzHvgqB>YdlzNc}KxVXvP0 z_(*S%Y~ShL)P_w0$IJGG<%4L@=VWa8VYf>zh08(ZYaHOVE{lsU8}_K=b>oke6?0oJ}V6N6;6F7ctq|i_fxyV zI&;(kL5|Pa8Zj7&>!K86c*aTsX`!_gM&X%g&Jvp>TjU_mrX0Wi zAubi1Q88`9wj+<-40i|h6*hv}Y{rx9Sjj^!d71SKyL&|>kkym`&8jUr+sCNB^;ObFvHlq*jk~Im#9oba1_R9KJrHt{(3uOc(VJ9FHZ^m z#aV8ax11jwKv&-++_>?AxJ_}KKA&^QZB46UIMKmUv@_xS_=`?ZlR+zC!H&#MUZ@+3@42%44<&(2^z65fkK}P@)Hb*a8ik^ zl+{F1l0G9@k&}wD*X;LKX0jrZ;zHMjq@WXBx@FQEf6|-_0voVI!66kdfXi2&v)tU{hQNw@%X`0TabQl zfU0c3rgHtbc82K~u0oUwA-IKVA(m-pco+2`;{#oN zZ`IO^$2e`VL+;)uu>l6-gKqD{LFx60V!r(K3qo_AJ(43305V zWQnGd;31%iP`{d5gd)|~l~d8H^eox*bZb<@q-)i!Dw@!28UgiyS^K87$CA(ThxX^q z*6YdfApg%-h>rU$zUR#Ax2x9mgEDslesR&{2LcAOxJfZUH12`v!8-0LK2@y5tM~Xa zz%B+rfytFR{Av=O9A!J`qSJXm9vj(#Ek_)!WN28^+)?6v-gnhyY4+EV7@gtIYI<08 zDANFC1BP}k-6MwPMw0) zqmUHIKU@2_(GhfTUa@iFWUAJRmSG5@vS+N0IiA?bvA4UM$Me7#YSs;L=m7yt+I>Ku zOh01C68JDzBh3b^qtWM)#nYLM=LLWMiEoH+r=7QKT@(xLP%dz>N+#0U>~YlKwRZjX z)n>z9jB(ooIxFiHJGOG=4_1+ueeNmYgdj1b5Lk`Gc`PNEDGmR2=qKWrr32~c8pv&n z?B{ipfP2=im<=}RaJ@KZwLX%=E>zm|?DvY+{^#E!STE3HBgbzg;p1WJ*;>tb>z2=o zpNYQ8={};y!GVv^N4E`ooCabqTB1B+%NhA%w!*```h;6Gq&Ep};^!$!$8eJjt4F^M zx19#o$Q6uRB05v7|1P}XmCMoxfr<{X;<5HekdjUiCmDs0254uw5>+pph&&iI!0O?z zE}?#(Fc<(2RSco$2eEnh>z@2=)Wa6lu_2M;!^_`Cv2Rc^YrocSiJX#rMJG4=xg|mWatiJEzg1~*p1o8)#eu(MyM;3NDMZ*$fzd_}N3(%zVL z@y13Tc8GAmk#-9O?_n<5)McImh>9MWf$kuOzi4*S73Peu&Q7PHJ5))_DUq$OuGNHp zGeivHxSJvN0DH@ zfp2-mia*c!Vp?@J+F!o@Cg*d+11|>TLL_olP%RPYv2--jaIYXE&2yJnmU=FI&b{!frj`QEc5NUmI9g0*Tf$VyHVxeh^ioT*r)yL^XRFderc&A?_>7s~@t(D&YSPZ=33A9g&~^n;F2CrGRidpo*w~ z%9kA(b_;^?*-hYZ=)ug3mq#*y?;kG&aoerpLreB4Hjj6L;|i&O;H%=Zo0Zy^LE#nx zTyW4fJm;Nv!YB5e3js!Hk4Ii8oT=m`IdpnN!Nu$H>&+@5j7`}7bK-!>j`+xQw-&QC zRgpcL7m&9lGdLAr+xp1J_p4L_><+dnh9a}jKQs>|^bi4zy8iTlKF3>v((oD;P?9_h zje^iUcBsKyCu?LkTEmW4QxcL<7FBMuO&)D+pjY?CWUPuyK!vv#iC^?Y5BlzcODEw9#0ei^ zix$#^(D+yC&hsMedCgRv57#(!v{rHI$16B^rB2{~=a_Vj84-dYjj!uhmWpL!Z zU-T%|anznc(gmtcIbd&czyQ<3HAzqdoQc}6Ky@;l_T&cW$``1S=lynWdZ)uU0FdwT> zDau17tQ*$J&AgcXr{CP2^sj;iPGNkbN-j&JJxsH`8)x)vUN6aO2?23gWc2C&pyZ8S z=U`HHqVPy&=5xK3P`-B!vh*%EA&--b1jFoU-@&ewgxpRRgxJ7{`0O{Qi(-YH2bT?2 z$W?0hrm-wK7w?x?Ws^k07THAl(Zm(q9N~l#1)fm_d7=X`DI%Rp;cWl7Y2gu+YmE`} zK_^M)%*o))6x6w6loXL#fc8KZaSYM4$OeXDx4@w1h1hQ~0hZgGD#uCn7IT`tbxh{>ya)TsdBseppc z9EKu<6)D|b2Yo z>@hDOn^DxY3;>UiWcu92w1_R6s0&8}cD& zPM6dhg=!W+UDF0P3+~dXdB~t`%{6%32Rz3@Uz;F1*v%X_{m4e>Dg7Wd&H=qX5j}(% zUroQBvucr$=QqhG^6NFS85v!g)4C9bu8TauZGtD8lO4aYW7GL_(xL>(oh92rak@aw z-54QXl`KzQq5|d3aT)Krch|*FaSe|v2fY5I(o5nh@o^dXf@Rp)UZ=gCmLJ9-||m0+=Z*1(Jk2}YwM z-20BckKuAlLeh>p*okDt%@j&pjK(3OM_hL@o#2Qrx)XHC3N@Uu{9|_f3)m{3dg@1> z$x}8OS<_fDSyQdn@Fvl%G(opC1DMsEQzboEo{@dbB#KFzp z)ym#N%HGkLote$IlOmz|dj39c(&WSLUVeG%`eRg25UCw+@%fuFu_@P?== z)W%^nl9ngJ&4K`}pG6wlU1l0r^t2fv-Q;n16RC9LEEc*J#b)ck{exCe^==kaLD9*d z@Kh&AD^&d{I%QlHbU&A#Wjo7+2jRhfhma}*dw&U%)2(xKLMrCI)g5Ynwg~<#ZB4eS zWL=Jgt%G*P2IMOkjy$GN!xeZAc^JijN6WU*4L4^N(GBQ=)50{sHg4k0hr z)TM%d`C{L#FvR6biV*74E~<|4m*)A40Q04o$bvS68%JNG;vOXl0Tpo?R4oF($AuV$ zFXO<4TNo;dK$(r{>eQ;I=jxZUykG{7M{(xm{YrFQTpcwa(rA3C%_i6$AO4D!jcybHh zP0iMVKOtb{_4eZ|ve@X{4-P8xP)q$kxC-W?h7}J-bM=z#^3{TOdJC@aUE74hFsrmZ z4`>DuC~;0cNbl(-X}9%y;iLA6F98smx_y|0?yMXGq>kx|KqkSAzm}@+ED8mh%G%6s z+6rfZ1tm#5${`0wkNZrkWLMjlYQN8S^Se6AigSJ@G0W$Gid9QD^~PN?S&>@G$uz}( zKrAuU1dwC}sU+0k3a)n+m93O_D3}Pp4zy=9L^{M5)WRKBtI*2ZD376i&y0YOWWjGl zVy4?hl9g5Pw{4T+?^q$wb&51ZO*1?A;SFmMet?y*8&H!a6XZd-Jlw^S5U(~#qBoa% z2`yL@%^n}B4wz}B_gpKP^FM-}${%^Y`tlWh=7>#9znl3*eL?l*Unl;p%o*(_k;qo; zEteng1PI`#S05MVN}NqLoKMcEICnOig-FgmI%a$dN#;y;OGwWhZ`+PLC@)5S-I&1q z)V@m<`PAAw9&!oZ`fe_RL(?k0tPs`623L!MI@<-{!bxlw3x+hYo!WeG{1dTG15KY= z-`lajz9pCcv)n@aZzER0&Gml|+5a@5$zUj{ma?1!pJPv&-Xm2E9HT>%gsMtHBn!=# zETtIUEY#rwIN9wW+_-QO3JKvNTKJ(F1NKAEop*K~FETi;Coit14gN7d8ZyCyB4sOt zEsQtPVB3EBwS$!F8qFwOHP)IahQ)#%X z^FhfQ-uJ<{^CTJ?BL)`|Kr_NrneFlvgmbq;oJdqp!i&UjMmb#l(~7M;5KMP4A>>y8 zaY`AV0H#jp)Iqq*Kyyib!}|zKFh>_{7Jzi3hg&(%bO*bdh4dq=X3mEN4_h6s(kN?e z8~odfo~m9CZ)5;2eZ_EQBG5d8QM&P6#Q31SZ@<%$`Jh+E`vh6}|gB+M{5kxOhql%+7j6AA8|q~8NQ3pZXy=R8TEO(MsXl8QOFGKQ*0VVMU-N6 zmk>r4`4%J{NsRWVGAmElTa3Ex1M9DV-eH_-mPKtO@DshOdZwHDfNvV9UNlAZ_ek`L zxhOl87Mb1Ic`}P?lG5P^ZwY)>KTKtAMW*%qSh`AhCVpvvw1;Fi6q=n?V|R7ulk7^{eM)yB25?%+(oy~>n`&~o&njU$^06|Eg-C^ zxD|*oh;B(KmaHrgK-HjOp~GA7_mU+?wx2opzJ#kqAQKXYT#pSqdSJ-7Uxc3)twq(S zCRmd|#QNLA{YrD&qCJTGsGWu(}?Xp%LCih4Me45&lu>B|pH z-(x{mBj6FunfwB@b4wc!)1c4QIx!v%$g>V4=2mct1-=;TGZ`rdqKjk6O( zHZCbPor6p8@YOdnK=MSj`pW%R{}I7%&w={wKKb{GgZfA;oY>y}hZ2(^16(f0+rZc) zcT=uLuS0ge!Y5T_=A;3OpA&HR+EqWhZpAJ)-m=LXtFu(Lo{&XPHPm*8)A1BjIPPk6 zzpwyZMFgbwB+8>&22&;KfA7mFC@& z|EP}fN^T4hEQ`5M>cUmIzQf*;I9>s=Vh-L6-LI%1LKEE|Ofu}FpBZi&zxY%+k@4{& zq3QIvWlrXSwlxx=AmX2eb`ub&K#RPS!Qp`-)^9G-xR2mRCCTDqy~LG1cw7X!i2DKwwcV+`4l3g z*vEV^f0pke;X&L`5+#`TDH3xN7JEfo-Z+6LU-pqgxV@(9X&+mno!va{g9On-%`1t0SJIyq)QK;55+dI@ z1+}Mv%=BW=T(E}Ozzc_U(V=EV925EuY6qz}eZqVvd!j)s?0uCjpqTKCF?SXpXv~ka zig{mE0Y2QEzEXFKfQD|f;wC4Xr^ifDHE)T68w}MiMcf>#@K=4 z6V(~k+*+TvCVt3>zvfA@wzZIuib4NQj_w)QQJ8O^u4s3!vC&#+vPM#Hu^?tYO-$bN z#wG0`U`A$oD5mHnm}UvW8Mv%*@4xhZv+QytauyRy-Jz3M5Y3Ao+&<&3r>lzXAVY}W zoW;Nu3%_I`7v0rd*yNZ>K8)un9FTy+@5@5hHb73NMlGzzkmJY27AubJ?|+%37j{@J zw~VgCDQxURe^FBP$_Ua%>a;2t2-wo1DhnJ{*DKbj(r>_XFV3w0EFLCwS36`E34Rk3 zItsn%OO|SQ$v(6Hv6aq;vHYW$(fHvk&eq&)I?hLc^ohkE#jUwW5i@umJ9PG1OGveBY{WGew zrRl4)n*&XE2~{~IV2de!45lNzZD-nr9Pa?korW8*jgT&AeM*YP3X}Ih3Vd0qWy;T; zE*Gv;+G{Q_#;E}Ry;fV72pV`RQSO!@gNglSm99y9O}jH`d-T z8{eI4#(~Iwt)XN(-6S-a?t_V|U-Mw-L5Hf%icv)w1yo=}ZX4?sSKtVFI%hCQgu%a& zPRcmXE0F&J{EtH%QNxyf8V`Cpzf_eAq*Gdd`~2r#OVAsY9{mT)Cx^@7EoLI46O0%6 zxWmw5k>mG0S>-5lC(+bjPGDPYMMUG8+MtU)3!jO!YZ+bbZ-J5P1Wb?gIG~!XJypO7cYt3_kq>2oE zxIWW{L<$RnDwA);aA4J~ofG^J;fi}Jz9#_u>#;#t#B5m_n2vX-!*w1#i#gTUIwC3v zb)auN^$ymYZwBTGw&jpk%jCh`qt|!-xfJZvG_7XbA-yFsr4FZAxodkBNgi zB#rL%FORnmzWomY=+mmfXYThWW^HC0mPvIfCcf%dxc6x*S=IU8qh`MeD)Oc?_CDxc1Olz_gGS$0mSQ&c`yUZhTYnAtCt zmiuL#E;uzDAG~vr$Y=5IK1!N#&EAe((6^NJo1ZvP3D&b4tLK=**;JN3oYi>Tes zN%%?{w9T&kCwz*X=0wB{`)BIsJJRwc3r0~+{b=Xd;Twj{z0IC6vzN!(^OmsB=Fpu% zq9@*0OOCfkXx8SyQS<0ZsrtUNMQxL1f$%QgaFkCC$X8!>KX!<|fVC zUSSlNXtxxqTfGT;fDoa~5m!{iVo(XKO;zyU`KV`zP74LYb3%>LBfP#jZlIX=#W2^g>|GLsaCLtCWjkl z-7aLjl+*T9J=*0S{|>xAKqxwBD2yvO91)AwQF?Qg2DyzV@3kn0w<-Vg$&Yg2Vvb8i zKBA_?*MUl!Q?A&aUL(Hv21;CwU#M-6oK@rvXOzyi8)r&V0buesm9)XOKP?Oi&MB2G zR$$7QSd2Nb&j%DmoQh0U-WD?o_J%C+HR0{{Qu2*9>6hkHciS#98$v}ejj^Hi z6E|p(b|lD%EM3ALd#o|z3$E<+|+CdR`Wj#bsxrMZ$@aT6*U+opt9;)U~#7=9C& z4{w{31y`bm2Ze~Xf@~8_z#jErPvspDa}a7}7_;rNI!gNlW*N-wF_NrsE91H8kO#(3 z%;zj&e3-}i{)9?3~;R5v_<|a&)iiz18?p{l^fQ%*e z&1!Yq7V{yJrK4sK%(;Ms@yZjp)#&ye-f9eXoM;Ac2BFLnbWOifp_*t8a3P!UFZ7~= zsOxt6mx)p^}e6x1>cctx#Jg)LKAu2xB_VbCTJEo_zw?Z+~1 zP(2ERHW-3)_4;k6RspS;FLMr9Th7vc)k69E#SaGn@cjF~>Oyb-Vxj); zJ^sb49YysVOr`W}36;$p%p7g(|0(2p$<#>?(87DI%(p@WIi4@p38>!667y1uqQvpy z$Qzv=gHkx5U>kCLv4V2@#_+F#K!k~J-d}HfxU|3Ay* zUse3yxQPGfa{X_;TgAdrQ3d6Dda}!bgaIkn&yP4EP?j`stU^jW7I>Z)+-N;ek+_gk z{VZ0qzt@m>T9V(Ns{6J|siJvazv>A=QL&szu?*SDtFgPvX2t&PQl=`VNz;7eHOt{< zvn{rgQrGC3>*TUwW_xG*gX4qy=X`}4(6o;R@V3{27m6=LMt5#+&^4bdNfeId35V4Z z_87zyE8)zrTcB5?!@1X2Czn0}#~Kq8yUpx2c5R+9S*AE$|LpTw@LJYJpAi7a58fXf z!qlDwuZgbB!LQdz=&4R|99z1O*(78{c+#=kLw^=~iiE)oC;~VcT;7ZI8pwjUm=@dhVI}*~}AzcG;T7{T`9ygnH^O9(rEx1HoQ8*;KB-k-ZC3GI-65zMzusX7;JckFAk%SyUo;E&w_C)3{h@EkG z(1bTJsQ%QJ1Uia5UMp}5griFY2YQm-HME3=Cw#`-IM^5xc?04Tw3~}I z`phf&+hBNWK?V?qH^FU1$mY?v41U!R<#Mi6CaGU}jork7Fy{0ki9n_!!ay1Pr@d2v zP~qnuNN)m?adCrzzS>v?P4fv;_?B^DyA$d59e!1S6DCKzn(2FlzIYp^?kQL9QEYak zbjb#}SU{(M1Xu%Apw>`%U|m6;N@T5U8B(H*jR9kd;m^1X+cR+05A+Tv_(fi>|N6O#AZI*cHQzlL789g++AtMto0Md=No9;1##M(!uY80h}UG8^JbMi$+>7$&$aAjWPe;75+G zzQ>Hat^X!rz>nO20_-ag+M|%)0`Y`(12Vvml)xINX~9o+PP2`&>X&AqQ>^_R5(ZCf zC)t}j3lAfUjFLd{5@=fkt^NDLlJ-`(cnJkx3|o|rY-jQm6*eYg&G1hWC$UPZwnj&b z)%MatPmjgJRYr&Ymj|$T2ff*{6g$uFiph!a$~;XQOOZ+!l^Oz)6DGDEw-=)a$r&q- zjt(_VjY9B4R~4BUPc%nE6BnBu-WTN?=pQ>5X$%HkV~5E^EjrYgRwbW;o3|Y=ccUY` zwk8S&K?1aA!Zb|-0wn zwmP5Ou7C|RZN5pt$R{q>z(38|yQ1`4N3ag$Clk>f1G_B1{rQc1jZY~?W( zguL#|@uN8S%wm4=uJ){1qcKKOYLj(MjNxFg#`AB2^jRHKjb)1D#_kZ6X$_pm?Bh9N zFlDh4t|zK1BUM6VCE!sar3(_~2A=B4z@iTXUl>qm@%#AtA_H&=bPEIVLNMj6 zQgj|-d$J!~)=15KW*YuB(ZD-Uh>G0t7%V70X%LA1x)>1ADleKaR812e4zjSo%gfVP zuYf0w5{glGfWNA`+ob@VREShXYlbTrh7nUxsao1hr0ofy5K#J+*mB?xjsX%FZo2?u zWZ9QbDr+07Imd_e&I?C5FX-jd>KNV5M*x|w(TVKYhE1{1LNr5$SYR8crU#m1i7$rBVY*G&yTZrtI1@J2G=`9Ze*mE|`y z#>{aucVDfl?>5-HLo7Pe4_x-OreC+Pdq$YNX+~}wqHYSDoq0TcA}(DP6RslN>%HV= zV9wS2UJih!yW#R9Z!YcMKJcd>?WAupBC|t6sms}7;z!sX_Xj?;0K~>{W^EY&PX|R< zA=UO>0pRZ6<|d4#$*EVCA{D;@T-;wRW5)?fWW9bBc~I-_Wk0kQS6X69{8?yq#*BMV z{dQy=A8~rVOK0~cQ}U}y7)~CEsZtHokd3(-k1R^o-++F5j^%GKl?||(9r&aOO5Jmg z`6WP#zN)s)%sn1AY@#HXv8t%n{H}1DRK;!JsHnsp%$vkKN5}MVQ_c`AwG>HbizUJp zSeTOjvHG?hP2@`T=Q63#N=_orKqJX2BiKH3KO=+#bv97BU!$>Q{I7S~c8VOi_OTOs zq3bLk;|$gO-MQ&uSua~$nrVPar;KBoW7dd<;$+| z(btg~Z4j&_u70Jg&#ReBX!V}m0&^4%i`+r9lgPNn$>8N&apsk!@K`uAYAyz=H;Jlp zY?rTuJS8WnZo@BQ5yaF(XY*>Q+HhknH+3w%FHYVJsf}bWecl7W-enRF_@GDF2C^21 zo`ExvPyKBNt5~7%;HWgf@!Yse?N>Rep;Urn+AIxUmEdxEB|Kp=@4(I3&$zy?-8$N% zcQqkBOenMT#Y^n?>p^4&pQ9Z1g<+lAZ-%wiv)=78eeyxR@gijZDvA$G4d8tN84A*i zK802CCSvvqT$M%$Fk{;MDP#>@P1^fN_kc z-$pzUGir8daB>wn_b(EEV|I5o8ttXrYef3r@04`}Rz+McCYowx+Iu8}wpTWdRcW5? z><=tgMUV?`7bl_QyO(xrmT6uD-y!YixF)r(t{qhIm~s2X*(~RRpX=de@Uf%DCyATx zE=lH`akHu&+mkGz=^Lg$@JB!LB47HOI>C|?&Me%(Hb*&hf!cfs-n==~Ji*>LqCeC| z@%CS4L()9)@R#9ikNARKvm&RX79ZU*v=_YPm?B+9Ll&DAd0H;v;BR^vyDl~($A-S(r+6}MoLm_{7rn4>gCRg z4*Qt&(cc$rE9Q{N^T#`1P;$zXO}%QyXJd8@*b0j7sl(cam;?P)~nLmuNxSDPYbS4mxZyZLIkLXe_r<>37h39?5&}sAo4DhMPC=!8i2;?$NMba8$3{ zZu?-B%Xsz1K7zJB{vEgekbTbSopG_^+Oe6PJANKL17jXI(%VP2_BjJ%bZ8_K!+rD5 zL?Rl$)-8;VX7o>tR@!b#N;3Pg>nyy3fCV|jt`HS(5>^qO5Rod>PcBWp$5y|?LBG?D zKZD)wG*qstQu%$yQf(3BDBF&flq%Ng&JRpQ+vN%03JE--1jR-_6itR>-E{*xd zck2WenI%j(hSd`}&8t!5N`AhEHO4%o+`@FsxMiY_I(H3ql*zaA>#I@Z zBpD_{9!VP&I2P2uRkUtQBrCpTv+d!VeM(Yj-=L7^($um4Oea6%Ne24V_iO8$e@@VC z8K~5Xv}ubLq86VrIKQ+ms&SlEa!oXapv2TSJ%y^PsVIwkM)(v+nodNixcN8cWw_!| zE~hqjN+noV<_sg)RNyWXIaSxXClfi<@3zW{kS}GF#Bt{>H0+>2)WlK$`Z+j7L7c9* z>@legtW~DxQh|cQ3`DyK42F$vp?84#ny@dyDG-Zp*sAQhna@L9_ETbls@<+mC(4Y| zF-)iU)uB~a5f8IzuY?a8jI*7m+BA5`W>nw%py*B`v96zGN1n{dCZ<^b$g2)tA&k}xO^+Gek zDj&NmAMopse}@o}q+C#9>2RuQG=$v2Bh^N#a(o1|Hfj^3!&;XVd#ViL10Lz5$e3-_ zc?O%c3+>aUffMaiXF1_J`$w_#kT$mpHnEnSDp{I&z_}dsVIT*5jcy&h(T`ruS0*_# zT)`ayuEEiT%&o$c=Pw?GH_Fv;jMuiB!=Hc9zU}XGl9IpuPO|^c6y?9tzTf|r_Q~4Y z*c#b8y8V;%C8@l5C?27B_o&%348Rzg(DBZO>Jek{l@h{3_>G>zmqD0{%2^Y$jP)8? zX9fW_iGx%q)~%bPs5hB{Y{*9-gC*9lF0ZR=Y^*D3taE=Z_w<&;@bBDYa3mPg^L_{% zZ%lh{J6w--pzUx1=^}jZ1p!IJP~%u5gCpC}HDg2}5U?-0did)Eic|flDh0t3{y-ROv-w5|*T4Je9YP;}x>9jjZB8fD#E& zN3$dyWc>BQOc2k=lqflyQ#>b2EKBd)F<<|u7-314&aah%ZQf>>Q4+1y`H~qvELA|b zkc&AHo30?NG6>TcQgTMAi5?>wI+c#bxK}Nd{Fl_)l022o@o8#=x$pow4pRXT;M3b@ znxoN#Ws+XE@`zI@0x32<5)J|ZE*fbC&P+bFMp5&d{00o0_&h{gemsV1?f@815@F?I z0k7MYA?my`|4Z(#D9m(6*E(6Ci7uL4q$PTiP*~s zN6CRGq#P@OF4AE{oSZ%>Xd_m3UF#mN*xphYO7PJVRGcbR;@vQkkAvp2q$WRJZG(t8 z;xU_K)^V9RUuPVOxj3Uh|}+%a~LZw8UbICEJSLO-)5- z^*RIsx@u1UNd7;`j71t+?A9af7TB3Ww4SBq(LLFh`x*=I2HJl>1xWaS~>Zl=p>o!820*5EtlZu==@EI4%@^DcqN=WcP6K@q3d~KE+Cp-OMCJSc(Hefet;NjM#s*{h_`)^c;Wzl!o!TAYDe3gzb5yy@JNeSr$En0gwd38Gic{e4RdmZ z2e4}=!Yoz*z{g+-eA0V~Q&TjLa-&c=r#M7?;iCaS20#_=mj=c zGYMN!^Py(pO*siqqMz++_ErE;WZH@59HPxle+|YYS&^Q-G19muO^dUaEcFCwO^ZWk zSi)F4r#}xGsiS4Nq(;~e9t+r35Ly;D$_}o!B>)NEn#=wc@yfQgRf87=qvDS~wvQDb zDYV!Bbcr;!m(A9goba?lp4Hq)IdPbCIg$*c^->5B#WlucOecoPAWZWTQp4mHHECop zjsmZ&>H72Z6FBY3YAY6z3fVSW?ElLlwpVZ4!~OOA3>Ho(N|1h-wn}SxfnJS&1DR#PuDX`X0dLLz?$>hi{I|w5uo^Db>E2+9ADM+9vN60JAL-mzOH#9w^lXGO4SC zw%`TWI7vI9|FRwVxX9KEvWZ02W%@)``r}A7O9C^2w&Y)i2|(8 zs8kpWwIItt2<*C_+*1F6M|%9c(a9BujQ6ULh5xFM2R3uJ8+%#;dFY1#lM&gXQ%J7i zzG3v4!T3Y1;FD52yMOcPlJtr zhk3d_z0!z@wTv>uKg|`$MxGx+y3gjq=8(L~`6IgE+f6OM zs%_HcQ15x1Z_#*L#C#a&!HcBs+;mpOT?5kMWxxSChbxj^=j?s*ZhFA>Et;H-S{!l} zBSVO*H>QcL^4+wGo$t5=gk}X3Fn1s0x3pFsLN33w zSF_m!im6}yej7a{nI(2IZn5plF#(%4=0PWALl?3=Z=+DrVNaaqhS#@%ZAUcxxb+DF z-recR&cyvLPe^c+yiC@-(~P&GsFjnrfhi9o8qh9hJkSy!JNKXuK}gyv?OtP0!Lc%^ zQQ(4cVr)~b{{{N*^#)(l5jMa?Ajs+=jd5O#_&=>oE#DC4p-U+ARo#@r%d2~-1}Tc{ z#0^BMVDolyNl6($gRP^JW5UNIuV(7y2VR&pcjZ(Hr2>g$LL6GaKXic#wU=1hvMZM4 zY7_;_L`n6RF9pTj(@vOo+9qapE|6k8aM9W&p2v1zehKyPL*QF52l zd8{G#$j_OB1k7eW5f+iII^6pt()(l?u67X~k*{fr_>%nWE zThq^t0bK?p=XQAMY#}rQUzCA=(*d*+J2M_VNPoo4T|+T(hrwI}-LrFxyIOrk(zZti zk)!Jr>ST})?Bs^$8sZBs965!mWFj!7%cWd}DF++$*l-+BkPnEik-?wxG5 zk@+B9JXxu)O1|Cf^0AVMsJKXr$9L%1w#Z(2llf7t)At@nW!)QK@2w)#W_i_qjnpQdoF$8{g!1b)fJ{_ThVK(9{H^vBfR-@(VhWp3G7 zC2Gj99xLydb~$Fm^q)Rb`evZXpVx7Mgz4%oKSp}Be!&f4^*r18E?DcAc6mA(edjFR z2hkjG!i6=CXpP1B^jhj8L+=^7QLLI8zDYyM<6PK#MGnIJ{{21RyxaFT{90ZjK>KLi zTp*#Q;(HP?#UZStUu(}D;o+QOb81lY*TiH^dW^_*M$EO{Li>Z0h|&s9!?jU$$SNyr zLGlM^XMW{FTyebUI|BoLhdt6ae@3oz+KoQacX@ZjjZuex=i96`kHTIjcub?_PVpPo>%8#6 zKSzhV)DZY3^9Iwu@)za#&OwxvAe!WfObZb0;yQ*ZBMA%=hJ@WpHxjREaALlaTC8Dv83q>LNQO4`si;*8lUn|}v1dl_{ocWr9MtIp`7 zSYX~`Uy#cFl2H?$!Csu~u9SY%DSFxqhJ-FD3YXB+YstXYSS9}AeFc_-UV010^vYYm zXOFEWLu*`0Cqy`-3&+g1`=mR1xsqc&i+5Ta==?dSEiAB3cI4YC8tI1!t!e4vO}jU6q~+r8 zhD53rIT4+e!1s2AzehYQxy!tS3g?!?{+?0t_E(bj;rsC0Y!Q8qza)=0)FBX(i7E)z z3U4NX2;2tyV}k=0Xuvus7wLs(;C94eU*fXbIxu||nU0TgYaBgn!Wey!>Wp|M@gL6H z;*UPMMSm+k7k@if|Fi1zulr5%-|V-8jg!5Bk)(}*-v6zTRVisGEbt;Gassb^x;D3MmKJY}!cdXH5loHF-%xB6&KBJ4s!!nW z9LV+ul0A2^t;S{}xOr%{OvQ4eR`98dCR-M(tDasm#ozjo_7M#xjzXi~mByaP%0`CD zLL+#}HqW|XMzEtG)b5e&bOZpz^lWIB&u=BN%YJAXCOEE@MxAf9SZUH7f>OXxyQU1; z1s;tx7+G_W2z1EC&=k$cMT3BZCROo1+`pn#xH~>!upi!{n6H`u{Rlk)co-vi!kMn2 z=tlZtV_32H&!l{DxCYflIw?|`2h}dikX4vD@CwRcobKs8#J+4gyTEL^C|Mz!HEC+% z(uK9HOT+8=?+7P_J}R{`ZF-dFm%uuW3JF@3?fg6qCMtuX`VO7GIw*d)4*ogr1-(sZ*lh2NFi&H45gnj&q^{0Xg{e zZ{nIRCsSL_at`*T=JuZ#`&O`ereo(1)md35M62zIUPp2Pg(wWr^QCPc4W-VSIX7lD zk?%c!wMIs{x-p|;ymirQ^DxhM%0i29=hN>3?-xsObG<(xB9%l7dF!+M3G)~Oj{bnL zJ@BcWL}zXp+w_d|m@`B@zFx&*zSjyX5QnrKQz_BkoBfqGovyE;q ztmq8dqTQ{n}N0cx~ zcOOV{2;UTCQ3a893xOg0`x%LAQ7Jo|!cf)8nC9p2l?4N@bO|Z8{@Zk}3jRM`;eTJa zl{a!WvUe~NlveoXyX~c8p)w#{{g=UdwR|2nA3o5|ktN`vCK82^h=-MGY9=U#UpzU(4<-yD2=JRfb zrso%WKt&!50!nx^={iRH%OnuPMQU}D7S;8g{OByVRfjX{0K8a~rFm$cKu~c3(mU39 zPzm2C1#}iF+={17&c~iYa0OX`7}2BEN;)PRMfe`4fa8cGV09jrOx$C+q+0dKgB!yxG@5$QnsS_O>*i~$1GaVAbft|F#U>0|582({7U=UdM7zr;t37ruoRc zSIWLN8hRaL(WcQ?yNg7XTi&3&1N?hvuMz|;dWpOfg)SF(>l(i9`Ae^Dx&UACA80kq z-9tX)z_YTB6Zc@*qMF7*saC8=bHG85`Vn|KIUs66ZC5Hy0o1H@*fLb z&>OehsUVbvFIqFr{gfU2Am^;D;{`1w`yp;)R=CB7eZ-_`^v9|6X%O&Dq?3{SgV{ z$;~tTE#;LWgo~tF)K(`9=SGOf4oim;P8zU~J&5gmn9%UJVyu^TOnw>f8D!>o&I1FYn+Ew@n7E`M}Ff0KLuADY0pJ2F2I3X6#8hJSo`HfNkmXlJd&T9SH-SW;d0;BG}QW*jY`x8}4bM?9t--m??l-Y!1i0Kio8n7ABXID;+|KPM#(ou_r{Ts z>l9!OdlmU?e}c8D!Y-+opiF_JqaUHnE#1rPY+5r1w3g*-Bljln+u@>%bp>sWzULwI z+Js=8pD<8Es?lQLq-;&6Q5eJEZkHLi@ZK0wf?OjvXsDm3dYm;k7{4|+@aO9rk2w2? zSX~%tVu-#(7|BBT)ehkrLpS(~a^elw@Uiuq;kd{v5iyYPu0#%ukc2SQ;1?=kMj^WnJe zmwtFb>>a7Wkctx9EFZ?lTrnrl*C#Xp8me%z{A(d9A|OiriaMHMtSD<{o{JY0X-MTa zPQ?#!f%Xms8Uw(@UcgR^mT!#|E5zFpQ?g;&<=`l026*^-&-h<&rw1x!)DBkl0C7sG ze*~#B$~dpl-Ky>J7y?lkSfgYPhnhJRH10@O%r+5p5*J8ZJ&ptM#T|=bMbNmISi4!W z#3GT+RG=HX&&hYi9-eQb1YG)<0Bj>hH5yjVUp=Sso^hnU|KY2A#l^ueHT`@~` zVc3p$9tp+3*^L=v&l8%jqIz~7hL!bk7CO;B68W4NJg4uej0uOuFThUI0}zj2E7|Rl z9z2(CplW@EhQ+oQBJ$fNOoa-TZ33&XUIT4z<1LE0fWh0EE)Ovydzl+JuzS1+vGgD& zA}K!KNpW5$IbxRj4_a@**9w;3qlZ`Z{3pJhR*jx<9IixQ@t1GyFKchG!D}`vYyJsH z{C)zYv;S^(>;Jr3XJU?tZY$qHiIzMkcJy{^NNG@Ka0TOp5nmw}#3?6^;{n!Yy zuR3WfOQrE)af~U#1DB#|&2&Q{(Ua)-P>|=M9BfE!1rYr+QV7<2%kH zrG9yn>|f04eHYS|DK`~Fnu0BvMEzB+al2aJYt88l#P5DUt3l>J66b@PxN9UY;Vx6#~!9IORguy7sHG=EJ~On zI{Xv~Co^y~#6tMDm}YS{Dx@%Ty-iwZMgFWyGgefz(B5}%(}aMEVP>IDTz}OJfiwe& z^dUH85ThJfdfMxN*lA4OGZxQ8&BMQy6&lX{S`6$%#`>i_Xi8j$MQ~gidZ2NtY|A^u z_b95`lUephH=~3*&bNC`ariKTMJDV3L2n-frE9-a#dHvwfg&(O2^*o`9{*ArgCG%i zC;}2x5qsc{C~Dn>&f=UhM@Y0nsTUGFGpHR!> z(ULp`W7*|{Hc$`54uxHsD#+|)JB5kKc+Zp{zS%H427;h~9FdiZJ{D_yUF54se5##@ zuDDk=4p5_6&ke`j%5keqZ%Wc=m6f2qjg)hKgG$c?0$V6Px-fZ)`hIS93KiE}fmfn$ zXi*SJ>`Qk%GMrA8QF;2$xd7}5a;A#iFUlx&%fK_yx?iX!!|6iW+=>8ku_;QO{t}ra z5EjSThy!EkW;ImIxl#G2xo|bdUm|MZ(b)n(0dWL#rY1MKI+v;?z+sNsb8+;l2m_ZZ z@oZZlu6z?XWvn79MlSucaL2>JkCS_!u2l$RlFt@tfvzn<^=#?CD=s?l*eW(yQ-jlR zw8@P0#or`^#bpH)S#kGhC9?EmshhF1GG$5z8*dnz_87n#8?V+*4B0DoO@>$`V;T(V z6e2_%#tw%>cHqAJt*}3u&9%&ysPjmi?q=Z89U@Jf&OrB`Wux&eJu<5imo;!JU>;i0 zPA&|#$=_;Kz!4#@9}OwxnZJI_IxDrSHHpWf)z`5qQ}d$K0@aK|jV>D4WmpMmB3SFC zNFSW5j#VH=2b}(D&G&DpJL9*^H+o{8CATjfiH)~pcW1C$0gr7(_gvrZlzY^kIX&ju zzj%5%Jd-1AAkD)Dboh#iXE9o%$9J6+f&EDa8d~J&fQ)u=$8W&Yk9Jv`1N*(%SV43u zHg39q%@CS@*6(=5XL!4GdfD$$^k9eX6yc{Fd1BE4cE|-WR~~^h;d;o$I5Y04U0)42 zUyd+@%C7gdoKVJI0nZDXs-RbB@b-7O!qqDC48~N#v)>t9C#zND*dFB{!Ec|44}&Et z_iGhR7D%R_s4M_^S7;W=9?#{_pZ|^Pab7e?9P?Zb?)TeeEcb5_?5)7KUH{18;Hx}D z3Z+Y=uH3gGShX}QJodvbaqDL@s@OoRaV+=kokwr5j z?Rm%l%p(JyAcjo!jC%+5+Bxjkf!fW^rMew|$_+W<3dHQ>&3@EJf_?C)%t2d|^Cs#t z_1hk0AP1bhbfPFU2J>dHmL*n`r3f{_g`s~qRs&4iF=sxMaNY*YGgf(^JfofEjy}J| zL;c3Fc89B^HnuC>kdR={3A*wqJHu%{+o|Kt7xVyR z31`>G-r(2J@~diC01r!!{ko6huj<#(@=hxQe?5BL9t)y@dILuQam|`wowCo=b0Dnq z)7*p(C=$QxpqMIVok$*2Rdtzr0Y!ZZ7qZE>mA$c}ZywoKPisPKn=qLKOJR?JsWdzi zi1RWOS(e!Fhg7=s1y~rC6+F&%XLUF+-Rl(ibIyTYjt#BE99)zweaI!|Lz01fJqdCI zQR=8d0>#7kB4r6n&Qhlb$^mA4%b4);lx|X7o55+G)Bo*p+BuV_hznj^L#5=NE$-(Z zU$Hb59~pAa8f)sA3^Ya^IdVfrF4=n%cJt&hxVSG&J0&-q;#xuC3Wge`D@2;VZ7B3- zIQ12N_jg5tmiG)}d$hEERTuYQlX=ZYbZ`~Q#I;LILA)#{|8BPM&mn&&@2>zUfX>Oo zp8n?6L4*g)lLMpVZ>Q;gmem@Yeb$rD-zA>OXC1u?DQVv=Z0$Qox86C1t;0)b*c?43 zZ9jvHKSA>}X`elIyHD$G-k?#N-4r~%EPCOg!(lsrV% z^fO43V^Z~J1^HxhSH4xwy~-TA6;`!F(>4bN8*e4gx}m}5pe4Cl0s^Jwo{$7gOAD+E zINt<|7@0V`RcT##+5T^8rRFcl-3Q%7hc%FIM>EE;ND(VT%?g83s3g`4Df`z)QS*SG5(7n7Jg2OohSfG&ybJ;w@#azPf_pD&pn0ByzKJ%U(&7+k*pBR6~B z%2zGpuke!jKZXqv{dZXBuUX80U>!obe}dZ}c`ca@T4XNk4q$#zwUO?T6M!VHSSiVL zVt7P;f%HTxQIPGeMe!(ktTkBqpQUHO1>9$#=Yry`La$Tx!9Q=?7X=R0Gf`h3Gc!5> z&UF~M!R;}^p&qyB`B2_N2bmR=-|KDgWd^s;Me*0ZdE4vUIepLrp43UJb^GyH*NqVH zy3-=)``pJrW^=fzB=67;X|^Q7I*B^Sk^7N~YunF^$iMr6@TTD5>PBnxrBGcK zzB?L8oDcfPJOw!m=B+!4e2QhEBdRpq-&`kaHT#qcgW{~=bI{$09OA;R|Mt!jn;nR{ zfDG{SA9#_ywLV0Af{>Pz|Wx;~97)WC!yk@2)jZBD)0TS&u9P z9$NQ&EEwR*F6350C4?KOxknCXUM4!GUl)iwSV4SIYRb5Hb?U3OJAek?Cdl_=A8o4Z z)rqqx01Xhh^E;_O+MB)vmZ#EF#I3OC?v$18kqOlR?i^qY*^2f$bmmTuPvC^@L=I{% z{P?ZeAf3Y*nA?;_!-{>`pPyP#N`~8TwEGo>H$^6q*XURAuaH^KLWbhxy?+D#gWm*u zC1*GOg}gNXqxr*s3wi&~G5F^ph*F2pRW!-^@p3qnBBs$pPwl0JFk!n#1XnS@`VHSd zmq;j!*b6`fJq8a-%wT6c&(Ebyv0SlKt)W7e^e5l0NxjlqG-BQzSu1AiX;aH)ODm>& z#a$zBZRf{>h9QAgZ-d!w=Og;(?E`N|qUUvgA>~)4JOq!XE!J)7ZN4DBEu*_jBR7K7 z^O6mZBja*{5~&)N%y5GgSazF-B-=DgP5I)LKWG#h>KM z3EVkU^Q0>W$;ly35FH)L4ST}$;=*pCs;Uol^&x*fN_dN%!dQPaKx5$(?QJY!Gx~5gAKR{ z_Npp(2sX+{V8YS#Az!2_W0lGuEjdg9&_$%5qK zVqR;D=*t2U&B$>BAqJ4uMiWA>-biAF$e(GJj9F8cVG@Rls1Dt`rGPcGaj2E89-s~R zGRlMK$kG|%=9+#UlGq~<`ayK6Yomdd9DJ1G;9p6-Kf_?9>@4_KIvBASNjGi3n>kABgp4j5E8ED4wrNHYFyG zfJ=A9+Lj(&k3h0yN=|#heANiD&Carlt6r>AjfWt;8rYdzSa$zAAIm`$_2qRpN^Pv5 z#kFaFpBN@0SY>aV8t$Le$g3S|#shS?wv2Qa2O-U3l0GR2zNvR-81<;QSiWpJ|>jR-H`BWU}v+ zI~1TF%YB{OnuEYI_V{QajLajhfHOzRG#n4S8x;@dsMxpku)kqJf~AWx!>oC;{rs_J z5<*T62)Hh_x?v?fK|Tz(H^kpSJH&qZ9hOVZ!}Mi+jf5+22^+*16U+arva^7yYUv(0 zsYoeO(nyDN3!-!j(n`n$F5O+Cpn$ZLfYJ!kh%^GyNJ@y(-6bvUe|YbG-^Z1U&;NPr zzU5-A^E-R??Af#TK67Ti4-QCHj5S`w@(5K+w;2tk827v2X*k)utN1c*9YVg+VrDe( zg_T##^pdzOBoHqE{jk&@9B!HOD%ML}NJ}W-fmr}SLE@Av@=xmZ!@vTjR1R|k|KzIO zwAvT7$IMv|mD2mIwn?8AnwOz3e7t` z@*L|9In+^o5^kcJ@~^H3jCa>0;>`}f!>Ba%p?Dlww1ISNHlF2g?s5-OLKo$aW4FHS zzdxIhph-uqDJ!Qaksni)Nw%;%B7fl#hrVqwHDp!Xv*5n7UZeNRQ45*vo=la&$rjCK zy`RaN70g-8mhV{la21wA(x*07<=x&(O>T9@m5?xrzxVv$PLPGJmCrY-=~wZ+sT_J} zl-|<5i!Jchm)ZXLJ;$dJdGbc0Qc;3hqp`s|q~@X6CbU>;bUOE#25|dgSiQ&egE657 zJF3f*l~o1eaj|m6Yknop zaWSPO+3Rk6JS-DbKeM~tgJ{x1DAF5NKcogdRlYtxKi-ypkv2xZkwajf*Ti_2riRRZms+rZ^_0$e{Rgg4^Lx#k}BdPuc0t9i?HS2t0$l%jvjw zaugbJ3lRzhUJmk+DRcFLm%h0?ws020HSXe}2On6ddecS(Y92^N2vYy3b4gE1-=B>A zO4dYsz-EW?O|+5(f;& ztt2)~k_v$u3wQ$rQhgzj$T3a&dXF;KwsTix_!Hd^;|E8y96pdTWGJ>xlTKCf#J)jp z7%RY>Eja#wa2(nCmLx=WM!j=Rn_;h{VJy`ndroD2p_o%cZ|39ArvZ zHjqn%GBNSpNWNK8d^gUIpu>W7IF3uzpOb-BhB=Wb3Ym0IjVO{+fYonFVi!{_?@J;F zZd#V8LIG6y^Ja{NYfnXG-XcyU3NxFkYbTXSxrWn?DkOu2E0u0}r7dP#&u&GNRId7l zE6IFv$L@nDW*9g_)g0E zELdrwRq}DhGk3*W8vO4v`2ox52l+436Ag)5>pYz^o{~p+|l5G;u`wu-PFrOtDky~o>n%FDS$PqM5N6Jwn!Az{E}--+I89X zN->E~>zpp4>#i1qP3f6<@jUePnjlfWb@teJn7U6a8FFgsvGLq+9Fw~IfK`(=3SUI; zD=(t@V#H;EO1gwfijN{ib2-F3vhB{%A>lYNJ|gCgR_YYpwRfMIkohtMZa(gyWu493 zyBkuG6+zaua(~Y3Jyl=A)B3Ws4-4F~iWS#9KjJ)z)Upb+BB~w4P)khtF0|3K>kzCS z-=xiab;HfguOOLmIrF_V<@8KvrWe3+#c;Z6qXUzvUgsmqxx z>KKhBgPtC`)E27mywxLI?%(;ges{^RX(5DmKZkiurT(P~dqTrY3_kXtg=B$>5%oE_ zqBeFbHvMQiI<|30%5pquRiIt<>ubV|+CR}&HW(Rd5&Vv0i7ybvb19`D%gT$t|N4!CWpSiC=SIzNcQQI(XM5+gp=6HDd`E=YY z#pB9mn#PIXc2P{N*nguXVRD7}&C{J7j4wxePmN8sKJ;~Nu@A1qh4;GiQX}tPfgZ3c z{KSzxzSmxdtN8?DYa#;GEw^o<_%2I&rEI9U7~xNy)YK*g;?fLSQde+jZDLQ*ylYZZ2d2sqPb zKBP;OAcPDiEMk2b_~6BdUW|DQd+tb8Ye({vYkG%k7JdfC4U(o&oM8zkd_{S^AGGfe zICFpCPcF!zE>oc{SK9dSm9=Ch%+$?p;azT9XW5kM8d8^2Uov|tm0ek6>m8N#HP^57 zVFs2(-iED%oXR#)`{YYAV>t;HsXpp!NlL}8^zA*A$4vJD2BF$`!Ai1AumT6Ch5xMIfuoWbv$KLSv^Jf~<7UIT+PCuKt6)5?_ zG(VMC!O@kdY06m*rt{j2|w?c3zoUsy{)+Kw`*t7M^GRp^z3Rg)i)EI8SIOE{g8xC zpFLz6hG`ZrB^5`tH>Da6@Gr>si;;dV$?6~FbTJBD;?zdvIKn+XW)uucEHFEYD>(G< z>QZM-XNiAWq~(Ym-ejIk?TMPD`ZRBUndnO!X^XGpHWKgG_iJAk>!!Aa67oeyvzpDN zY<@7JnEU6t3+%mu#F5r*EvhhN&n&(68;_Vn-h1~t-#zRh7q(}8)`6BTlBd+QW3M02 zZ}&Jno(u*DK6VYoM0loFNQfy)mS#!D)ADBVke0_&^0fAcKWpXXke*>v#hxXOn-s9Q zblFj>*15~`a_K|w`~#}dJ`uF)1H6(A+XsWvEyPGAPa41Ko33%_dhB3AuG4>A^8Yc2 z+Z%mhtodF{CiKDDk(-r+;R3ra)s1%D7EbD2KcRkNQ=Rerpsno6>pBMM?qP!=8@eb? z#>-++fnGCByt?gep<@{;DmhEn7~x-RMhl<(p5 zY6@nza=GG*cwYoq#?P?lK`OlbFn9AH$>SVrgeclW{8ts-4V!d*<(i5nY@OnGv2VuV z3HG9Q#ZtINqSC!fc-qS@G!>Cm?b*7*8a-e{U|4ds?5UMZN=|YI_R4FrL1?8Z@VP$5 z>;6KOmUb!_pP~eQ#47IvhCO*diZq?@~lzIsf9pvr+*hl->+jgq0HMUSQwhI{8L zItn{Inhl9q6R+Oe)T1xntL(_<7s;dE+2`)f6fj&3M^gd5y4jeVVj2 zYoNYn_CW$-#ft3dIhL)t!9)?1ak5u#mMkY_4K))JIo8QE8t06!u}glH)Ha?dW|gkE zGQ&2x;H04vU(~LT=V$ju)0eH#pR=W^${2@CmbglM3~+9(Wru z8Wo6TD!0Yzs=r&&Zjr4*U+U|SXT$1m`!d@6W}VoPxz~f5T-Dr0Hn$@xP`F)1D_(sj zXzDid_a4g}{a3=7w-?J?I4TrZKB*lM>`w1HW6j7@m>uT~IVy8Z6h{Oo`hSG7%j!?j z#e&0k39h{%Sxq7tCcgZ1$2 z8h`99hG@57aBgz5SA8hIowC9zo!3@eAGpmkB5CKNO@Wz~{_#%s;;6$|OwnSm5WUY% zpn8%m%FjM^_W^ryufaCNu>p>OoElE|#;%mm1H9dLM~q63Z!rq6wXOPIJ=(e6k18ck zJGg*?I9!*Q^-kllU>K9xv!I%QX1$F9<%q<0C5KgOo3=}PB;dT+gR3=;1whzAxwsEf2i4ggBg@BL3(yhAC!qA7nUx|M@;Yxw@3?U9x)D zzEBJ%|28HcV>RN$i2=3@krj+RPiIoF=j~IC@q%ujVH1)Ak)wMkOKTU3A3krgpp04O; zcgYntRW`6L8&~l4mh8)jG=0IM+8lInWE0%h(8q4Z=*vbI9(b9F+(|#wxR}J$JF1f- z7a1?N`GI%FNx~ld|J^+K!8l3Ad833i-Q? z|4;o^6D(QyMs%#f>`Ht8eW(vdh=r}fbL;*K$cUKOB?ZA(ArbvC*XTLS-B|)Zg)`^+ z+jDU{_A-|jE0DNdQW%nGxnB6yA~Jz z11-XQ#bKGQPRu1yRfQj)_C|W$^0=aV6YqV}(zrd?5pFK3D#zi|I(bxY;PbwMdoZc% zCH_|zqn;Rw7_N)y74Pk1TRc7=KI1S?e@_(5%UCqP!x7IL?pDDS4UWy>Oy^>gCNag( za$sYw*2;%Ye7!QF=x8I&WUw>)o(t88;69#)|I3#yP=n{MOkRsZs4dr;j_kRnBtvq8 zH*fene;Zy=B>tuwdktUQRGH5)B)r1mKujNv>rg?0N^kf2ID@j-Eg8E{TB~o7i5;KK zi`>jm@)=S|51WoZDs*GD*2_>*!&!Neb}w3#M7_Fz`-hP|SH+O6Uxrlb=#}kmh)=6V zy7Sv@A%byf4V!r0J#-C zS#Z>4e)sM8>%oRa3Dh{M+wmQ-Vyo|y3~d^1A;swC_~_-W7s&InNrDMCnO?qm_uVw! zV3G*c$FJ8yp*wRzL?zK>@wqVbjM)}9{eg+bz4|DXvL+tZu!+pNdY$DbUqUAx7;I`E z`F~dO8yt8n!Mm-Hv@yezXU`-rTFl%(xFJ^15%$%dA-+Z1$;zob=9$W#FgbmYd{10S zK-;A}^}W>cFQy~h752VXMjVCht~7)-d!0|OhxmMMfAFJ%AKfb`_+V-LnFP(nSD(ntu3r z;bNhE`K711-an&Ns$(yi;&$F%6=Ydq?}zX z_d9W6i*!78Lxm)jExc7k+1>~4>-I6tuN} z;2arIpA}I`Pp%l6`y2dEKL|e&(pnvf9^AOl!031NqkfG&H#63!a?D*)$(M-vF9;ut zlaN@g=I_z^gl3Qxyeehv!83o(^Dy<(cuH1f;g&7#_mozh6!ut?YpGM(i35OUIOJj8G|KhRIftUEo*g@51JRAD)dD zmgV`r9^E6Ue&%*(CBw38kC%tHDG6iLB*U_>YRZkOIglKDS1=@FwP)Zm#xH|E&#q-c zY8_2)%BFC01NjTnjQfk)4x1#XG~q3_P0duNp|Frc($o1R!vbk8#cZy1}iQ}arTNFRT zO?Nb;P7cm>ggDFlh4>^_h4+$fJu`TZ?Po~8dXTh+Fg${|@<mO zH4;!~h{|RqqD!JG$0+UNG`A_)&X}K>yY7-L`Gk()UB?wX;Iv169is$yt&8sX_pO_tI<8+J(bR1`y z`ElB2Mh(>L)t%Qv?0!m$l$V+y9rjM-HHPi`2off;RqSSO@wnSYkC@bTn}4xM;QlT+ zt)`*eNwwz{NMjMvE+?wEAW|~Dggr)G5;JJ>#&tp=D?suAL8ppqk@s;R#CE?_XRFv} z>z>TxPTFq}FZ$4^qKbZ+17PsxuzL8X2IN~E+72|alzzAH{4c_2mZQZDI@ui(AB~vk8c9BdC zyGXF}O?>B$g@WOCRCGz|1A;r`I@j~Vbyl2fesp7aUM_xI(>8LDH!i-?;lW@!7FHV@ zJ$mn_6uzwguv^0dUjD}8<+Yv**YoZjG>RJvx=EJW`Aiaqy0FUDU7fy5uS-gm5s8e- zdHl@!ILP|g+gkVz>!Dg}|8s#TP1l#BDEl}gn_D^yeoXrb7DSsb`-Rt7lRMlErIu*E zOd)0*2s3%7tyYZCyAh*|`jLx}l)gfBUL#W|kX$8bmbxfJXmX2hamhh{n&}p~>m+(` zZ9oAtcWthgQ^XIQ5yM-CxJP8Wmr>@apC8d%raw*|@O-Ax`B;!FEyziLEX{8;yoaY~ z8DpMhIPOQ!<@gXP=_&=NOW0&Wj|ax;dMY?TW;o$n8qu@>%QFk6ToWdJ!9o9iFIK9- zE}>`3L?6nGpN|YPH6RaPZy;aA)X!f=i?31cM#c2E($geGdg)KDS3ZBsDV95|a)RRC zEir0cZDDQNGHz{8ZSRZTQli+%m?FN)zE{Lzl7o`tMA%i?#jky3SLMusX6S1+D|Ogh z7K=>wO@4X3>0JT@xn9y&0rNFc3HxxH4Mn}W?*Y-`wM8Q0Ys5sau9a|ba}}`{Wj$HZ zt-+>9%0+png1o$e?1Vvnn{v07yjF210KtZW&CJ)mY&m zkza-Ag)$)T1lZ|OC+&88-ZD&D!{6CS2`*}IqkaIU6C1Rzy_O8X&U^t*Qc6goc80n#WB;Bj|RR51H8DkdasJ#t-x9n%zJ)&c| z4vyyDvLR%ZWm5T;d5aUvY@r1SaXpNwSXuaCN&n8H{ubKP?V8f|M_vi2L&Z-eyhO`) zjE4%aHlr%MBWpB^W@A?Ks)=uQ8hmzYarmxQe-q8(vw{fzb9YFRsK|p>BNX4EAe;b) zk`Hnfx|rjU-nHDShNyHxk2S`umOu0CRj_GL3pQ z$D&uHTt9FTYO8me~|s*B;K@|?jK3^N_jdXsK>*^tb;xvcC% zN8#Wt(36IMQiWKrS1S*)CyrmvSjT8#GY`D_#vWg>Fl0B%O6Ps+KuIra=c8OK;qw93 z{IYB^uXZ=EJmNOzWh#BtXcRjgC-mnmp1w~8O=pt5vzwjs9n~|+%Wn)-7S_*e-pE+e zo#(qz&0O(0ev6-(k-f#Kd6&smf567bc2*`1D%3n+-Z@1oRkT3Y(9UNlEan=%Vwv(i zn|BlE=sWc%*CruI4NKF|e4~(^re4JAI8rt)%T;Z0qUZ0?xn)1ew>GhN2w|)TvL6#8 zqZ{F+q;XVJ=E-p>3dj};X|nI4RfIca5heayRr zg{C|7{`{Nhx?5LnvoqMlx_TEghb_Bk@fs@F5fm+SE90g$_GlTt;e0C`*?gYj>RG7~cKslY(SHlsZSS!d&dP zpA;~_HDomz@xPZ{!EElDt8Q+C3qF3cdf$@1Aex-ksP{~n=?!C-EKZR`QIZKIo~da; z(me0|K_6}}Llv)mg0b(Vl%4!H#eQyG#>9JAUjr6MCHpg>P439R4<>n zdlB*F`XF`QkqF%4Qx`*Qvb}C88NKux1-)*Eb%+e#@v3S@G8*oD4i^s3x7R%b(`zs0 zQ!3Il`n1%0EabE#13t*I-)l4__K2ZjhwzuB-K*&)y~@x6$xI6)IOG>=G{jO-Vq)jG zC>@%Hg_xEsKuWMS!%2L$c8OB`UfG=*e1F8*d=ppu^76wq)?sG+b*+^sw0F|i$-F6a~|ywW)!+3!AP9+c1vHK=IaYPK&xXZUmFFK7eF0c`NxFL4`7IP^X8IwE~$+QkLDfabyWNult z{rWN%@?vjuL}>5&1aV$>Cp7n1WGJSS%fJVp!hiaZC6)U2onO1-rATl*;c||vf|0v+ zX^#x~;c~KqZS9&FY2JNJdL=Uc%-11K44ei0I6Q_Vyi<~56@?wrc|Ak=U+{c!ac+H0 zp;r+2%!Hp^Z^-e|PwKJ8mx0`!M7k2IMZVsvyNVY{kGS(#7Y=_~keT755%RvVA-@&6l2m2c$akS%q?;SR+xzZLC9v3t_=Rl^y;82mLsfF|p&8lu!

@b%UeX)%%HL!=}Ro{yrW{dEEl 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 l;Y8BYpANyco1OA^)c + + + server + org.dubhe + 0.0.1-SNAPSHOT + + 4.0.0 + + dubhe-data-dcm + 医学影像 + + 1.8 + 5.19.1 + + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + org.dubhe.biz + file + ${org.dubhe.biz.file.version} + + + org.dubhe.biz + data-permission + ${org.dubhe.biz.data-permission.version} + + + org.dubhe.biz + redis + ${org.dubhe.biz.redis.version} + + + + org.dubhe.cloud + registration + ${org.dubhe.cloud.registration.version} + + + + org.dubhe.cloud + configuration + ${org.dubhe.cloud.configuration.version} + + + + org.dubhe.cloud + swagger + ${org.dubhe.cloud.swagger.version} + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.dubhe + dubhe-data + ${org.dubhe.dubhe-data.version} + + + org.dcm4che + dcm4che-core + ${dcm4che.version} + system + ${project.basedir}/lib/dcm4che-core-5.19.1.jar + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + true + exec + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + true + src/main/resources + + + + + \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/DubheDcmApplication.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/DubheDcmApplication.java new file mode 100644 index 0000000..025f20f --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/DubheDcmApplication.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.dcm; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @description 医学模块服务启动类 + * @date 2020-12-16 + */ +@SpringBootApplication(scanBasePackages = "org.dubhe") +@MapperScan(basePackages = {"org.dubhe.dcm.**.dao"}) +public class DubheDcmApplication { + + public static void main(String[] args) { + System.setProperty("es.set.netty.runtime.available.processors", "false"); + SpringApplication.run(DubheDcmApplication.class, args); + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/constant/DcmConstant.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/constant/DcmConstant.java new file mode 100644 index 0000000..b11e872 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/constant/DcmConstant.java @@ -0,0 +1,83 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.dcm.constant; + +/** + * @description 常量 + * @date 2020-04-10 + */ +public class DcmConstant { + + public static final String MODULE_URL_PREFIX = "/"; + + /** + * medicineId + */ + public static final String MEDICINE_ID = "medicineId"; + + /** + * count + */ + public static final String COUNT ="count"; + + /** + * status + */ + public static final String STATUS ="status"; + + /** + * dcm文件路径 + */ + public static final String DCM_ANNOTATION_PATH = "dataset/dcm/"; + + /** + * dcm自动标注文件名 + */ + public static final String DCM_ANNOTATION = "/annotation/finished_annotation.json"; + + /** + * dcm自动合并标注文件名 + */ + public static final String DCM_MERGE_ANNOTATION = "/annotation/merge_annotation.json"; + + /** + * 文件分隔符 + */ + public static final String DCM_FILE_SEPARATOR = "/"; + + /** + * seriesInstanceUID + */ + public static final String SERIES_INSTABCE_UID = "seriesInstanceUID"; + + /** + * StudyInstanceUID + */ + public static final String STUDY_INSTANCE_UID = "StudyInstanceUID"; + + /** + * annotation + */ + public static final String ANNOTATION = "annotation"; + + /** + * dcm文件上传 + */ + public static final String DCM_UPLOAD = "ssh %s@%s \"docker run --rm -v %s:/nfs dcm4che/dcm4che-tools:5.10.5 storescu -c DCM4CHEE@%s:%s %s\""; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataLesionSliceMapper.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataLesionSliceMapper.java new file mode 100644 index 0000000..ee1daf6 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataLesionSliceMapper.java @@ -0,0 +1,48 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.dubhe.biz.base.annotation.DataPermission; +import org.dubhe.dcm.domain.entity.DataLesionSlice; + +/** + * @description 病灶信息 Mapper 接口 + * @date 2020-12-22 + */ +@DataPermission(ignoresMethod = {"insert"}) +public interface DataLesionSliceMapper extends BaseMapper { + + /** + * 根据数据集ID和病灶序号删除 + * + * @param id 病灶id + */ + @Delete("DELETE FROM data_lesion_slice WHERE id = #{id}") + void deleteByMedicineIdAndOrder(@Param("id") Long id); + + /** + * 根据数据集ID删除 + * + * @param medicineId 数据集ID + */ + @Delete("DELETE FROM data_lesion_slice WHERE medicine_id= #{medicineId}") + void deleteByMedicineId(@Param("medicineId") Long medicineId); + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineFileMapper.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineFileMapper.java new file mode 100644 index 0000000..9668b6e --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineFileMapper.java @@ -0,0 +1,123 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.dubhe.biz.base.annotation.DataPermission; +import org.dubhe.dcm.domain.entity.DataMedicineFile; + +import java.util.List; +import java.util.Map; + +/** + * @description 医学数据集文件管理 Mapper 接口 + * @date 2020-11-12 + */ +@DataPermission(ignoresMethod = {"insert","saveBatch"}) +public interface DataMedicineFileMapper extends BaseMapper { + + /** + * 根据医学数据集ID获取文件url列表 + * + * @param medicineId 数据集ID + * @return List 文件url列表 + */ + @Select("select url from data_medicine_file where medicine_id = #{medicineId} and deleted = 0") + List listMedicineFilesUrls(@Param("medicineId")Long medicineId); + + + + + + + /** + * 查询文件状态取并集 + * + * @param medicalIds 数据集ID + * @return 文件状态列表 + */ + List> getFileStatusCount(@Param("medicalIds")List medicalIds); + + /** + * 批量更新 文件状态 + * + * @param fileIds 文件ID集合 + * @param status 文件状态 + * @return int 更新数量 + */ + @Update("") + int updateStatusByIds(@Param("fileIds") List fileIds, @Param("status") Integer status); + + /** + * 查询文件集合 + * + * @param fileIds 文件ID集合 + * @return List 文件集合 + */ + @Select("") + List selectByIds(@Param("fileIds")List fileIds); + + + /** + * 批量插入医学数据集文件中间表 + * + * @param dataMedicineFiles 医学数据集文件中间表数据 + */ + void saveBatch(@Param("dataMedicineFiles") List dataMedicineFiles); + + /** + * 更新修改人ID + * + * @param medicineId 医学数据集id + * @param userId 当前操作人id + */ + @Update("update data_medicine_file set update_user_id = #{userId} where medicine_id = #{medicineId}") + void updateUserIdByMedicineId(@Param("medicineId") Long medicineId, @Param("userId") Long userId); + + + /** + * 更新数据集删除状态 + * + * @param id 数据集ID + * @param deleteFlag 数据集状态 + */ + @Update("update data_medicine_file set deleted = #{deleteFlag} where id = #{id}") + void updateStatusById(@Param("id") Long id, @Param("deleteFlag") Boolean deleteFlag); + + + /** + * 根据医学数据集id删除医学数据集文件数据 + * + * @param id 医学数据集ID + */ + @Delete("DELETE FROM data_medicine_file WHERE medicine_id= #{id} ") + void deleteByDatasetId(@Param("id") Long id); +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineMapper.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineMapper.java new file mode 100644 index 0000000..bc15f85 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/dao/DataMedicineMapper.java @@ -0,0 +1,89 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.dubhe.biz.base.annotation.DataPermission; +import org.dubhe.dcm.domain.entity.DataMedicine; + +/** + * @description 医学数据集管理 Mapper 接口 + * @date 2020-11-11 + */ +@DataPermission(ignoresMethod = {"insert"}) +public interface DataMedicineMapper extends BaseMapper { + + /** + * 根据序列实例UID查询医学数据集 + * + * @param seriesUid 序列实例UID + * @param originUserId 资源拥有人 + * @return DataMedicine 医学数据集 + */ + @Select("select * from data_medicine where series_instance_uid = #{seriesUid} and origin_user_id = #{originUserId} and deleted = 0") + DataMedicine findBySeriesUidAndNotId(@Param("seriesUid") String seriesUid, @Param("originUserId") Long originUserId); + + + /** + * 更新数据集状态 + * + * @param id 数据集ID + * @param status 数据集状态 + */ + @Update("update data_medicine set status = #{status} where id = #{id}") + void updateStatus(@Param("id") Long id, @Param("status") Integer status); + + + /** + * 更新数据集删除状态 + * + * @param id 数据集ID + * @param deleteFlag 数据集状态 + */ + @Update("update data_medicine set deleted = #{deleteFlag} where id = #{id}") + void updateStatusById(@Param("id") Long id, @Param("deleteFlag") Boolean deleteFlag); + + /** + * 根据id删除数据集 + * + * @param id 医学数据集id + */ + @Delete("DELETE FROM data_medicine WHERE id= #{id} ") + void deleteByDatasetId(@Param("id")Long id); + + /** + * 根据序列实例UID查询医学数据集 + * + * @param seriesInstanceUid 序列实例UID + * @return DataMedicine 医学数据集 + */ + @Select("select * from data_medicine where series_instance_uid = #{seriesUid} and deleted = 0") + DataMedicine findDataMedicineBySeriesUid(@Param("seriesUid") String seriesInstanceUid); + + /** + * 根据医学数据集ID查询被回收的医学数据集 + * + * @param datasetId 医学数据集ID + * @return DataMedicine 医学数据集 + */ + @Select("select * from data_medicine where id = #{id} and deleted = 1") + DataMedicine findDataMedicineByIdAndDeleteIsFalse(@Param("id") Long datasetId); +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionDrawInfoDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionDrawInfoDTO.java new file mode 100644 index 0000000..a1e1fa0 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionDrawInfoDTO.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 数据病损提取信息 + * @date 2020-12-28 + */ +@Data +public class DataLesionDrawInfoDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("标注信息id") + private String drawId; + + @ApiModelProperty("所在层面") + private Integer sliceNumber; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceCreateDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceCreateDTO.java new file mode 100644 index 0000000..502f32d --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceCreateDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @description 病灶层面信息CreateDTO + * @date 2020-12-22 + */ +@Data +public class DataLesionSliceCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("病灶序号") + private Integer lesionOrder; + + @ApiModelProperty("层面信息") + private String sliceDesc; + + @ApiModelProperty("标注信息") + private List list; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceDeleteDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceDeleteDTO.java new file mode 100644 index 0000000..4fd1ad7 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceDeleteDTO.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 病灶信息删除DTO + * @date 2020-12-23 + */ +@Data +public class DataLesionSliceDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为空") + private Long id; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceUpdateDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceUpdateDTO.java new file mode 100644 index 0000000..6433d9e --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataLesionSliceUpdateDTO.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * @description 病灶层面信息UpdateDTO + * @date 2020-12-23 + */ +@Data +public class DataLesionSliceUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "id", required = true) + @NotNull(message = "id不能为空") + private Long id; + + @ApiModelProperty(value = "lesionOrder", required = true) + @NotNull(message = "lesionOrder不能为空") + private Integer lesionOrder; + + @ApiModelProperty(value = "sliceDesc", required = true) + @NotNull(message = "sliceDesc不能为空") + private String sliceDesc; + + @ApiModelProperty("标注信息") + private List list; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedcineUpdateDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedcineUpdateDTO.java new file mode 100644 index 0000000..c288b7d --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedcineUpdateDTO.java @@ -0,0 +1,42 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * @description 医学数据集修改DTO + * @date 2020-11-26 + */ +@Data +public class DataMedcineUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "名称") + @NotBlank(message = "名称不能为空") + private String name; + + @ApiModelProperty(value = "描述") + private String remark; + + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineCreateDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineCreateDTO.java new file mode 100644 index 0000000..29b2ac6 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineCreateDTO.java @@ -0,0 +1,78 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.dcm.domain.entity.DataMedicine; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * @description 医学数据集创建DTO + * @date 2020-11-16 + */ +@Data +public class DataMedicineCreateDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty("检查号") + @NotBlank(message = "检查号不能为空", groups = Create.class) + private String patientID; + + @ApiModelProperty("研究实例UID") + @NotBlank(message = "研究实例UID不能为空", groups = Create.class) + private String studyInstanceUID; + + @ApiModelProperty("序列实例UID") + @NotBlank(message = "序列实例UID不能为空", groups = Create.class) + private String seriesInstanceUID; + + @ApiModelProperty(value = "模式") + @NotBlank(message = "模式不能为空", groups = Create.class) + private String modality; + + @ApiModelProperty(value = "部位") + private String bodyPartExamined; + + @ApiModelProperty(value = "名称") + @NotBlank(message = "名称不能为空", groups = Create.class) + private String name; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "标注类型") + private Integer annotateType; + + public @interface Create { + } + + /** + * 实体装换方法 + * + * @param dataMedicineCreateDTO 医学数据集创建DTO + * @param userId 用户ID + * @return 医学数据集实体 + */ + public static DataMedicine from(DataMedicineCreateDTO dataMedicineCreateDTO,Long userId) { + DataMedicine dataMedicine = new DataMedicine(dataMedicineCreateDTO,userId); + return dataMedicine; + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineDeleteDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineDeleteDTO.java new file mode 100644 index 0000000..278b958 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineDeleteDTO.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 医学数据集删除DTO + * @date 2020-11-17 + */ +@Data +public class DataMedicineDeleteDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ids", required = true) + @NotNull(message = "id不能为空") + private Long[] ids; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineFileCreateDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineFileCreateDTO.java new file mode 100644 index 0000000..abd0ed3 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineFileCreateDTO.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; + +/** + * @description 医学数据集文件DTO + * @date 2020-12-11 + */ +@Data +public class DataMedicineFileCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("dcm文件路径") + @NotEmpty(message = "dcm文件路径不能为空") + private String url; + + @ApiModelProperty("SOPInstanceUID") + @NotEmpty(message = "SOPInstanceUID不能为空") + @JsonProperty(value="SOPInstanceUID") + private String SOPInstanceUID; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineImportDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineImportDTO.java new file mode 100644 index 0000000..53938da --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineImportDTO.java @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * @description 医学数据集导入DTO + * @date 2020-11-16 + */ +@Data +public class DataMedicineImportDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("dataMedicineFileList") + @NotEmpty(message = "dataMedicineFileList不能为空") + private List dataMedicineFileCreateList; + + @ApiModelProperty("医学数据集ID") + @NotNull(message = "医学数据集ID不能为空") + private Long id; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineQueryDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineQueryDTO.java new file mode 100644 index 0000000..f84a3f2 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/DataMedicineQueryDTO.java @@ -0,0 +1,78 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.db.annotation.Query; +import org.dubhe.biz.db.base.PageQueryBase; + +/** + * @description 数据集查询 + * @date 2020-04-10 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@ApiModel +public class DataMedicineQueryDTO extends PageQueryBase { + private static final long serialVersionUID = 1L; + + @ApiModelProperty("ID") + public String id; + + @ApiModelProperty("名称") + public String name; + + @ApiModelProperty("序列实例UID") + @Query(type = Query.Type.EQ, propName = "series_instance_uid") + public String seriesInstanceUID; + + @ApiModelProperty("检查号") + @Query(type = Query.Type.LIKE, propName = "patient_id") + public String patientID; + + @ApiModelProperty("研究实例UID") + @Query(type = Query.Type.EQ, propName = "study_instance_uid") + public String studyInstanceUID; + + @ApiModelProperty("状态") + @Query(type = Query.Type.EQ, propName = "status") + public Integer status; + + @ApiModelProperty("检查类型(模式)") + @Query(type = Query.Type.EQ, propName = "modality") + public String modality; + + @ApiModelProperty("检查身体部位") + @Query(type = Query.Type.EQ, propName = "body_part_examined") + public String bodyPartExamined; + + @ApiModelProperty(value = "类型 0: private 私有数据, 1:team 团队数据 2:public 公开数据") + @Query(type = Query.Type.EQ, propName = "type") + private Integer type; + + @ApiModelProperty(value = "标注类型: 1001.器官分割 2001.病灶检测之肺结节检测") + private Integer annotateType; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAnnotationDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAnnotationDTO.java new file mode 100644 index 0000000..5305385 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAnnotationDTO.java @@ -0,0 +1,60 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dubhe.biz.base.constant.NumberConstant; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * @description 医学自动标注DTO + * @date 2020-11-12 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MedicineAnnotationDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("医学数据集ID") + @NotNull(message = "数据集ID不能为空") + private Long medicalId; + + @ApiModelProperty("标注信息") + @NotNull(message = "标注信息不能为空") + private String annotations; + + @ApiModelProperty("操作类型 0:保存 1:完成") + @NotNull(message = "操作类型不能为空") + @Min(value = NumberConstant.NUMBER_0) + @Max(value = NumberConstant.NUMBER_1) + private Integer type; + + @ApiModelProperty("要保存的dcm标注文件id") + private List medicalFiles; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAutoAnnotationDTO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAutoAnnotationDTO.java new file mode 100644 index 0000000..d4e87d2 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/dto/MedicineAutoAnnotationDTO.java @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @description 医学自动标注DTO + * @date 2020-11-12 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MedicineAutoAnnotationDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("医学数据集ID") + @NotNull(message = "自动标注的数据集不能为空") + private Long medicalId; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataLesionSlice.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataLesionSlice.java new file mode 100644 index 0000000..571b7ad --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataLesionSlice.java @@ -0,0 +1,69 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.dubhe.biz.db.entity.BaseEntity; + +import java.io.Serializable; + +/** + * @description 病灶层面信息 + * @date 2020-12-22 + */ +@Data +@RequiredArgsConstructor +@TableName("data_lesion_slice") +@ApiModel(value = "DataLesionSlice 对象", description = "病灶层面信息") +public class DataLesionSlice extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + @TableField(value = "id") + private Long id; + + @ApiModelProperty("病灶序号") + private Integer lesionOrder; + + @ApiModelProperty("病灶层面") + private String sliceDesc; + + @ApiModelProperty("数据集ID") + private Long medicineId; + + @ApiModelProperty("标注信息") + private String drawInfo; + + @ApiModelProperty(value = "资源拥有人id") + private Long originUserId; + + public DataLesionSlice(Integer lesionOrder, String sliceDesc, Long medicalId, String drawInfo, Long originUserId) { + this.lesionOrder = lesionOrder; + this.sliceDesc = sliceDesc; + this.medicineId = medicalId; + this.drawInfo = drawInfo; + this.originUserId = originUserId; + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicine.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicine.java new file mode 100644 index 0000000..7b0da8c --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicine.java @@ -0,0 +1,101 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.biz.db.entity.BaseEntity; +import org.dubhe.data.machine.constant.DataStateCodeConstant; +import org.dubhe.dcm.domain.dto.DataMedicineCreateDTO; + +import java.io.Serializable; + +/** + * @description 医学数据集 + * @date 2020-11-11 + */ +@Data +@TableName("data_medicine") +@ApiModel(value = "DataMedicine 对象", description = "医学数据集") +public class DataMedicine extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + @TableField(value = "id") + private Long id; + + @ApiModelProperty("检查号") + private String patientId; + + @ApiModelProperty("研究实例UID") + private String studyInstanceUid; + + @ApiModelProperty("序列实例UID") + private String seriesInstanceUid; + + @ApiModelProperty(value = "0:未标注,1:手动标注中,2:自动标注中,3:自动标注完成,4:标注完成") + private Integer status; + + @ApiModelProperty(value = "模式") + private String modality; + + @ApiModelProperty(value = "部位") + private String bodyPartExamined; + + @ApiModelProperty(value = "名称") + private String name; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty("资源拥有人") + private Long originUserId; + + @ApiModelProperty(value = "类型 0: private 私有数据, 1:team 团队数据 2:public 公开数据") + private Integer type; + + @ApiModelProperty(value = "标注类型: 1001.器官分割 2001.病灶检测之肺结节检测") + private Integer annotateType; + + public DataMedicine() { + } + + /** + * 转换DataMedicine对象 + * + * @param dataMedicineCreateDTO 创建数据集所需参数 + * @param userId 用户ID + */ + public DataMedicine(DataMedicineCreateDTO dataMedicineCreateDTO, Long userId) { + this.patientId = dataMedicineCreateDTO.getPatientID(); + this.studyInstanceUid = dataMedicineCreateDTO.getStudyInstanceUID(); + this.seriesInstanceUid = dataMedicineCreateDTO.getSeriesInstanceUID(); + this.modality = dataMedicineCreateDTO.getModality(); + this.bodyPartExamined = dataMedicineCreateDTO.getBodyPartExamined(); + this.status = DataStateCodeConstant.NOT_ANNOTATION_STATE; + this.name = dataMedicineCreateDTO.getName(); + this.remark = dataMedicineCreateDTO.getRemark(); + this.originUserId = userId; + this.annotateType = dataMedicineCreateDTO.getAnnotateType(); + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicineFile.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicineFile.java new file mode 100644 index 0000000..88bc77a --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/entity/DataMedicineFile.java @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.biz.db.entity.BaseEntity; + +import java.io.Serializable; + +/** + * @description 医学数据文件 + * @date 2020-11-11 + */ +@Data +@TableName("data_medicine_file") +@ApiModel(value = "DataMedicineFile 对象", description = "医学数据文件") +public class DataMedicineFile extends BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + @ApiModelProperty("医学数据集ID") + private Long medicineId; + + @ApiModelProperty("医学数据集名称") + private String name; + + @ApiModelProperty("文件地址") + private String url; + + @ApiModelProperty("资源拥有人") + private Long originUserId; + + @ApiModelProperty("文件状态") + private Integer status; + + @ApiModelProperty("instanceNumber") + private Integer instanceNumber; + + @ApiModelProperty("sopInstanceUid") + private String sopInstanceUid; + + @ApiModelProperty("imagePositionPatient") + private Double imagePositionPatient; + + public DataMedicineFile() { + + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataLesionSliceVO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataLesionSliceVO.java new file mode 100644 index 0000000..80a9d0b --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataLesionSliceVO.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.dcm.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @description 数据病变切片 + * @date 2020-12-28 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DataLesionSliceVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + @ApiModelProperty("病灶序号") + private Integer lesionOrder; + + @ApiModelProperty("病灶层面") + private String sliceDesc; + + @ApiModelProperty("标注信息") + private String list; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineCompleteAnnotationVO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineCompleteAnnotationVO.java new file mode 100644 index 0000000..066332f --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineCompleteAnnotationVO.java @@ -0,0 +1,41 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @description 医学数据集完成标注文件VO + * @date 2020-11-17 + */ +@Data +public class DataMedicineCompleteAnnotationVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("序列实例UID") + private String StudyInstanceUID; + + @ApiModelProperty("研究实例UID") + private String SeriesInstanceUID; + + @ApiModelProperty("完成标注文件") + private String annotations; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineVO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineVO.java new file mode 100644 index 0000000..68d26c1 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/DataMedicineVO.java @@ -0,0 +1,93 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.dubhe.dcm.domain.entity.DataMedicine; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @description 医学数据集详情VO + * @date 2020-11-17 + */ +@Data +public class DataMedicineVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("医学数据集id") + private Long id; + + @ApiModelProperty(value = "0:未标注,1:手动标注中,2:自动标注中,3:自动标注完成,4:标注完成") + private Integer status; + + @ApiModelProperty("检查号") + private String patientID; + + @ApiModelProperty("研究实例UID") + private String studyInstanceUID; + + @ApiModelProperty("序列实例UID") + private String seriesInstanceUID; + + @ApiModelProperty(value = "模式") + private String modality; + + @ApiModelProperty(value = "部位") + private String bodyPartExamined; + + @ApiModelProperty(value = "创建时间") + private Timestamp createTime; + + @ApiModelProperty(value = "更新时间") + private Timestamp updateTime; + + @ApiModelProperty(value = "名称") + private String name; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "标注类型") + private Integer annotateType; + + /** + * 医学数据集 转化 医学数据集详情VO 方法 + * + * @param dataMedicine 医学数据集实体 + * @return 医学数据集详情VO + */ + public static DataMedicineVO from(DataMedicine dataMedicine){ + DataMedicineVO dataMedicineVO = new DataMedicineVO(); + dataMedicineVO.setId(dataMedicine.getId()); + dataMedicineVO.setStatus(dataMedicine.getStatus()); + dataMedicineVO.setPatientID(dataMedicine.getPatientId()); + dataMedicineVO.setStudyInstanceUID(dataMedicine.getStudyInstanceUid()); + dataMedicineVO.setSeriesInstanceUID(dataMedicine.getSeriesInstanceUid()); + dataMedicineVO.setModality(dataMedicine.getModality()); + dataMedicineVO.setBodyPartExamined(dataMedicine.getBodyPartExamined()); + dataMedicineVO.setCreateTime(dataMedicine.getCreateTime()); + dataMedicineVO.setUpdateTime(dataMedicine.getUpdateTime()); + dataMedicineVO.setName(dataMedicine.getName()); + dataMedicineVO.setRemark(dataMedicine.getRemark()); + dataMedicineVO.setAnnotateType(dataMedicine.getAnnotateType()); + return dataMedicineVO; + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/ScheduleVO.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/ScheduleVO.java new file mode 100644 index 0000000..f0755f0 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/domain/vo/ScheduleVO.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description 医学数据集状态VO + * @date 2021-01-13 + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@Builder +public class ScheduleVO { + + @ApiModelProperty("未标注") + private Integer unfinished; + + @ApiModelProperty("标注完成") + private Integer finished; + + @ApiModelProperty("自动标注完成") + private Integer autoFinished; + + @ApiModelProperty("标注中") + private Integer manualAnnotating; +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateCodeConstant.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateCodeConstant.java new file mode 100644 index 0000000..a324a06 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateCodeConstant.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.constant; + +/** + * @description 数据集状态码 + * @date 2020-09-03 + */ +public class DcmDataStateCodeConstant { + + private DcmDataStateCodeConstant() { + } + + /** + * 未标注 + */ + public static final Integer NOT_ANNOTATION_STATE = 101; + /** + * 标注中 + */ + public static final Integer ANNOTATION_DATA_STATE = 102; + /** + * 自动标注中 + */ + public static final Integer AUTOMATIC_LABELING_STATE = 103; + /** + * 自动标注完成 + */ + public static final Integer AUTO_ANNOTATION_COMPLETE_STATE = 104; + /** + * 标注完成 + */ + public static final Integer ANNOTATION_COMPLETE_STATE = 105; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateMachineConstant.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateMachineConstant.java new file mode 100644 index 0000000..a142398 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmDataStateMachineConstant.java @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.constant; + +/** + * @description 状态机事件常量 + * @date 2020-08-27 + */ +public class DcmDataStateMachineConstant { + + private DcmDataStateMachineConstant() { + } + + /** + * 数据集状态 + */ + public static final String DCM_DATA_STATE_MACHINE = "dcmDataMedicineStateMachine"; + + /** + * 标注事件 标注中/自动标注完成/完成/未标注-->保存-->标注中 + */ + public static final String ANNOTATION_SAVE_EVENT = "annotationSaveEvent"; + + /** + * 标注事件 未标注-->自动标注-->自动标注中 + */ + public static final String AUTO_ANNOTATION_SAVE_EVENT = "autoAnnotationSaveEvent"; + + /** + * 标注事件 注中/自动标注完成/完成/未标注-->完成-->标注完成 + */ + public static final String ANNOTATION_COMPLETE_EVENT = "annotationCompleteEvent"; + + /** + * 标注事件 标注中-->自动标注-->自动标注中 + */ + public static final String AUTO_ANNOTATION_EVENT = "autoAnnotationEvent"; + + /** + * 标注事件 自动标注中-->自动标注-->自动标注完成 + */ + public static final String AUTO_ANNOTATION_COMPLETE_EVENT = "autoAnnotationCompleteEvent"; + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateCodeConstant.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateCodeConstant.java new file mode 100644 index 0000000..3522a66 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateCodeConstant.java @@ -0,0 +1,46 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.constant; + +/** + * @description 数据集状态码 + * @date 2020-09-03 + */ +public class DcmFileStateCodeConstant { + + private DcmFileStateCodeConstant(){ + + } + + /** + * 未标注 + */ + public static final Integer NOT_ANNOTATION_FILE_STATE = 101; + /** + * 标注中 + */ + public static final Integer ANNOTATION_FILE_STATE = 102; + /** + * 自动标注完成 + */ + public static final Integer AUTO_ANNOTATION_COMPLETE_FILE_STATE = 103; + /** + * 标注完成 + */ + public static final Integer ANNOTATION_COMPLETE_FILE_STATE = 104; + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateMachineConstant.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateMachineConstant.java new file mode 100644 index 0000000..cef3272 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/constant/DcmFileStateMachineConstant.java @@ -0,0 +1,52 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.constant; + +/** + * @description 文件状态事件常量 + * @date 2020-08-31 + */ +public class DcmFileStateMachineConstant { + + private DcmFileStateMachineConstant() { + } + + /** + * 文件状态 + */ + public static final String DCM_FILE_STATE_MACHINE = "dcmFileStateMachine"; + + /** + * 文件事件 标注中/自动标注完成/完成/未标注-->保存-->标注中 + */ + public static final String ANNOTATION_SAVE_EVENT = "annotationSaveEvent"; + + /** + * 文件事件 标注中/自动标注完成/完成/未标注-->完成-->标注完成 + */ + public static final String ANNOTATION_COMPLETE_EVENT = "annotationCompleteEvent"; + + /** + * 文件事件 未标注-->自动标注文件-->自动标注完成 + */ + public static final String AUTO_ANNOTATION_EVENT = "autoAnnotationEvent"; + /** + * 文件事件 未标注-->自动标注文件-->自动标注完成 + */ + public static final String AUTO_ANNOTATION_SAVE_EVENT = "autoAnnotationSaveEvent"; + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmDataStateEnum.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmDataStateEnum.java new file mode 100644 index 0000000..669a075 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmDataStateEnum.java @@ -0,0 +1,124 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.enums; + +import org.dubhe.dcm.machine.constant.DcmDataStateCodeConstant; + +/** + * @description 数据集状态类 + * @date 2020-08-28 + */ +public enum DcmDataStateEnum { + /** + * 未标注 + */ + NOT_ANNOTATION_STATE(DcmDataStateCodeConstant.NOT_ANNOTATION_STATE, "notAnnotationDcmState","未标注"), + /** + * 标注中 + */ + ANNOTATION_DATA_STATE(DcmDataStateCodeConstant.ANNOTATION_DATA_STATE, "annotationDataState","标注中"), + /** + * 自动标注中 + */ + AUTOMATIC_LABELING_STATE(DcmDataStateCodeConstant.AUTOMATIC_LABELING_STATE, "automaticLabelingDcmState","自动标注中"), + /** + * 自动标注完成 + */ + AUTO_ANNOTATION_COMPLETE_STATE(DcmDataStateCodeConstant.AUTO_ANNOTATION_COMPLETE_STATE, "autoAnnotationCompleteDcmState","自动标注完成"), + /** + * 标注完成 + */ + ANNOTATION_COMPLETE_STATE(DcmDataStateCodeConstant.ANNOTATION_COMPLETE_STATE, "annotationCompleteDcmState","标注完成"); + + /** + * 编码 + */ + private Integer code; + /** + * 状态机 + */ + private String stateMachine; + /** + * 描述 + */ + private String description; + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getStateMachine() { + return stateMachine; + } + + public void setStateMachine(String stateMachine) { + this.stateMachine = stateMachine; + } + + DcmDataStateEnum(Integer code, String stateMachine , String description) { + this.code = code; + this.stateMachine = stateMachine; + this.description = description; + } + + /** + * 根据CODE 获取 DESCRIPTION + * + * @param code 数据集状态编码 + * @return String + */ + public static String getStateMachine(Integer code) { + if (code != null) { + for (DcmDataStateEnum dcmDataStateEnum : DcmDataStateEnum.values()) { + if (dcmDataStateEnum.getCode().equals(code)) { + return dcmDataStateEnum.getStateMachine(); + } + } + } + return null; + } + + /** + * 根据CODE 获取 DataStateEnum + * + * @param code 数据集状态编码 + * @return String + */ + public static DcmDataStateEnum getState(Integer code) { + if (code != null) { + for (DcmDataStateEnum dcmDataStateEnum : DcmDataStateEnum.values()) { + if (dcmDataStateEnum.getCode().equals(code)) { + return dcmDataStateEnum; + } + } + } + return null; + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmFileStateEnum.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmFileStateEnum.java new file mode 100644 index 0000000..e657faa --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/enums/DcmFileStateEnum.java @@ -0,0 +1,138 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.enums; + + +import org.dubhe.dcm.machine.constant.DcmFileStateCodeConstant; + +import java.util.HashSet; +import java.util.Set; + + +/** + * @description 文件状态枚举类 + * @date 2020-08-28 + */ +public enum DcmFileStateEnum { + + /** + * 未标注 + */ + NOT_ANNOTATION_FILE_STATE(DcmFileStateCodeConstant.NOT_ANNOTATION_FILE_STATE, "notAnnotationDcmFileState","未标注"), + /** + * 标注中 + */ + ANNOTATION_FILE_STATE(DcmFileStateCodeConstant.ANNOTATION_FILE_STATE, "annotationFileState","标注中"), + /** + * 自动标注完成 + */ + AUTO_ANNOTATION_COMPLETE_FILE_STATE(DcmFileStateCodeConstant.AUTO_ANNOTATION_COMPLETE_FILE_STATE, "autoAnnotationCompleteDcmFileState","自动标注完成"), + /** + * 标注完成 + */ + ANNOTATION_COMPLETE_FILE_STATE(DcmFileStateCodeConstant.ANNOTATION_COMPLETE_FILE_STATE, "annotationCompleteDcmFileState","标注完成"); + /** + * 编码 + */ + private Integer code; + /** + * 状态机 + */ + private String stateMachine; + /** + * 描述 + */ + private String description; + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getStateMachine() { + return stateMachine; + } + + public void setStateMachine(String stateMachine) { + this.stateMachine = stateMachine; + } + + DcmFileStateEnum(Integer code, String stateMachine , String description) { + this.code = code; + this.stateMachine = stateMachine; + this.description = description; + } + + /** + * 根据CODE 获取 DESCRIPTION + * + * @param code 文件状态编码 + * @return String + */ + public static String getStateMachine(Integer code) { + if (code != null) { + for (DcmFileStateEnum dcmFileStateEnum : DcmFileStateEnum.values()) { + if (dcmFileStateEnum.getCode().equals(code)) { + return dcmFileStateEnum.getStateMachine(); + } + } + } + return null; + } + + /** + * 获取所有文件状态值 + * + * @return + */ + public static Set getAllValue() { + Set allValues = new HashSet<>(); + for (DcmFileStateEnum fileStatusEnum : DcmFileStateEnum.values()) { + allValues.add(fileStatusEnum.code); + } + return allValues; + } + + /** + * 根据CODE 获取 状态 + * + * @param code 文件状态编码 + * @return String + */ + public static DcmFileStateEnum getState(Integer code) { + if (code != null) { + for (DcmFileStateEnum dcmFileStateEnum : DcmFileStateEnum.values()) { + if (dcmFileStateEnum.getCode().equals(code)) { + return dcmFileStateEnum; + } + } + } + return null; + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/proxy/DcmStateMachineProxy.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/proxy/DcmStateMachineProxy.java new file mode 100644 index 0000000..be979e4 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/proxy/DcmStateMachineProxy.java @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.proxy; + +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.statemachine.dto.StateChangeDTO; +import org.dubhe.biz.statemachine.utils.StateMachineProxyUtil; +import org.dubhe.dcm.machine.statemachine.DcmGlobalStateMachine; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @description 代理执行状态机 + * @date 2020-08-27 + */ +@Component +public class DcmStateMachineProxy { + + /** + * 获取全局状态机Bean + * + * @return Object + */ + public static Object getGlobalStateMachine() { + return SpringContextHolder.getBean(DcmGlobalStateMachine.class); + } + + /** + * 代理执行单个状态机的状态切换 + * + * @param stateChangeDTO 数据集状态切换信息 + */ + public static void proxyExecutionSingleState(StateChangeDTO stateChangeDTO) { + StateMachineProxyUtil.proxyExecutionSingleState(stateChangeDTO,getGlobalStateMachine()); + } + + /** + * 代理执行多个状态机的状态切换 + * + * @param stateChangeDTOList 多个状态机切换信息 + */ + public static void proxyExecutionRelationState(List stateChangeDTOList) { + StateMachineProxyUtil.proxyExecutionRelationState(stateChangeDTOList,getGlobalStateMachine()); + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDataMedicineState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDataMedicineState.java new file mode 100644 index 0000000..839d91c --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDataMedicineState.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state; + + +import org.dubhe.dcm.domain.entity.DataMedicine; + +/** + * @description 数据集状态类 + * @date 2020-08-27 + */ +public abstract class AbstractDataMedicineState { + + /** + * 数据集事件 未标注-->标注数据集-->标注中 + * + * @param medical 医学数据集对象 + */ + public void annotationSaveEvent(DataMedicine medical) { + } + + /** + * 数据集事件 未标注-->自动标注-->自动标注中 + * + * @param medical 医学数据集对象 + */ + public void autoAnnotationSaveEvent(DataMedicine medical) { + } + + /** + * 数据集事件 标注中-->自动标注-->自动标注中 + * + * @param primaryKeyId 业务ID + */ + public void autoAnnotationEvent(Long primaryKeyId) { + } + + /** + * 数据集事件 自动标注中-->自动标注-->自动标注完成 + * + * @param primaryKeyId 业务ID + */ + public void autoAnnotationCompleteEvent(Long primaryKeyId) { + } + + /** + * 数据集事件 自动标注完成-->标注-->标注完成 + * + * @param medical 医学数据集对象 + */ + public void annotationCompleteEvent(DataMedicine medical) { + } + + + + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDcmFileState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDcmFileState.java new file mode 100644 index 0000000..c53526f --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/AbstractDcmFileState.java @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state; + + +import java.util.List; + +/** + * @description 文件抽象类 + * @date 2020-08-27 + */ +public abstract class AbstractDcmFileState { + + /** + * 文件事件 未标注-->自动标注文件-->标注中 + * + * @param fileIds 文件ID + */ + public void annotationEvent(List fileIds) { + } + + /** + * 文件事件 未标注-->自动标注文件-->自动标注完成 + * + * @param fileIds 文件ID + */ + public void autoAnnotationSaveEvent(List fileIds) { + } + + /** + * 文件事件 标注中-->自动标注文件-->自动标注完成 + * + * @param fileIds 文件ID + */ + public void autoAnnotationEvent(List fileIds) { + } + + /** + * 文件事件 标注中/自动标注完成/完成/未标注-->保存-->标注中 + * + * @param fileIds 医学数据集文件ID + */ + public void annotationSaveEvent(List fileIds){ + } + + /** + * 文件事件 标注中/自动标注完成/完成/未标注-->完成-->标注完成 + * + * @param fileIds 医学数据集文件ID + */ + public void annotationCompleteEvent(List fileIds){ + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationCompleteDcmState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationCompleteDcmState.java new file mode 100644 index 0000000..e343f1f --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationCompleteDcmState.java @@ -0,0 +1,73 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.datamedicine; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineMapper; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.machine.enums.DcmDataStateEnum; +import org.dubhe.dcm.machine.state.AbstractDataMedicineState; +import org.dubhe.dcm.machine.statemachine.DcmDataMedicineStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * @description 标注完成状态类 + * @date 2020-08-27 + */ +@Component +public class AnnotationCompleteDcmState extends AbstractDataMedicineState { + + + @Autowired + @Lazy + private DcmDataMedicineStateMachine dcmDataMedicineStateMachine; + + @Autowired + private DataMedicineMapper dataMedicineMapper; + + /** + * 数据集 标注完成-->标注-->标注中 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationSaveEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dataMedicineMapper.updateStatus(medical.getId(), DcmDataStateEnum.ANNOTATION_DATA_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationDataState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + + + /** + * 数据集 标注完成-->完成-->标注完成 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationCompleteEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationCompleteDcmState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationDataState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationDataState.java new file mode 100644 index 0000000..5eecf99 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AnnotationDataState.java @@ -0,0 +1,86 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.datamedicine; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineMapper; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.machine.enums.DcmDataStateEnum; +import org.dubhe.dcm.machine.state.AbstractDataMedicineState; +import org.dubhe.dcm.machine.statemachine.DcmDataMedicineStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +/** + * @description 标注中 + * @date 2020-11-17 + */ +@Service +public class AnnotationDataState extends AbstractDataMedicineState { + + @Autowired + @Lazy + private DcmDataMedicineStateMachine dcmDataMedicineStateMachine; + + @Autowired + private DataMedicineMapper dataMedicineMapper; + + + /** + * 数据集 标注中-->自动标注-->自动标注中 + * + * @param primaryKeyId 业务ID + */ + @Override + public void autoAnnotationEvent(Long primaryKeyId) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", primaryKeyId); + dataMedicineMapper.updateStatus(primaryKeyId, DcmDataStateEnum.AUTOMATIC_LABELING_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAutomaticLabelingDcmState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + + /** + * 数据集 标注中-->标注-->标注中 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationSaveEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationDataState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + + /** + * 数据集 标注中-->完成-->标注完成 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationCompleteEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dataMedicineMapper.updateStatus(medical.getId(), DcmDataStateEnum.ANNOTATION_COMPLETE_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationCompleteDcmState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutoAnnotationCompleteDcmState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutoAnnotationCompleteDcmState.java new file mode 100644 index 0000000..dde76ca --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutoAnnotationCompleteDcmState.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.datamedicine; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineMapper; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.machine.enums.DcmDataStateEnum; +import org.dubhe.dcm.machine.state.AbstractDataMedicineState; +import org.dubhe.dcm.machine.statemachine.DcmDataMedicineStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * @description 自动标注完成状态类 + * @date 2020-08-27 + */ +@Component +public class AutoAnnotationCompleteDcmState extends AbstractDataMedicineState { + + @Autowired + @Lazy + private DcmDataMedicineStateMachine dcmDataMedicineStateMachine; + + @Autowired + private DataMedicineMapper dataMedicineMapper; + + /** + * 数据集 自动标注完成-->标注-->标注中 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationSaveEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dataMedicineMapper.updateStatus(medical.getId(), DcmDataStateEnum.ANNOTATION_DATA_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationDataState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + + /** + * 数据集 自动标注完成-->完成-->标注完成 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationCompleteEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dataMedicineMapper.updateStatus(medical.getId(), DcmDataStateEnum.ANNOTATION_COMPLETE_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationCompleteDcmState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutomaticLabelingDcmState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutomaticLabelingDcmState.java new file mode 100644 index 0000000..9d6f2c8 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/AutomaticLabelingDcmState.java @@ -0,0 +1,57 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.datamedicine; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineMapper; +import org.dubhe.dcm.machine.enums.DcmDataStateEnum; +import org.dubhe.dcm.machine.state.AbstractDataMedicineState; +import org.dubhe.dcm.machine.statemachine.DcmDataMedicineStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * @description 自动标注中状态类 + * @date 2020-08-27 + */ +@Component +public class AutomaticLabelingDcmState extends AbstractDataMedicineState { + + @Autowired + @Lazy + private DcmDataMedicineStateMachine dcmDataMedicineStateMachine; + + @Autowired + private DataMedicineMapper dataMedicineMapper; + + /** + * 自动标注中 自动标注中-->自动标注->自动标注完成 + * + * @param primaryKeyId 业务ID + */ + @Override + public void autoAnnotationCompleteEvent(Long primaryKeyId) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注中】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", primaryKeyId); + dataMedicineMapper.updateStatus(primaryKeyId, DcmDataStateEnum.AUTO_ANNOTATION_COMPLETE_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAutoAnnotationCompleteDcmState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注中】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/NotAnnotationDcmState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/NotAnnotationDcmState.java new file mode 100644 index 0000000..4499a2b --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/datamedicine/NotAnnotationDcmState.java @@ -0,0 +1,85 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.datamedicine; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineMapper; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.machine.enums.DcmDataStateEnum; +import org.dubhe.dcm.machine.state.AbstractDataMedicineState; +import org.dubhe.dcm.machine.statemachine.DcmDataMedicineStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * @description 未标注状态类 + * @date 2020-08-27 + */ +@Component +public class NotAnnotationDcmState extends AbstractDataMedicineState { + + @Autowired + @Lazy + private DcmDataMedicineStateMachine dcmDataMedicineStateMachine; + + @Autowired + private DataMedicineMapper dataMedicineMapper; + + /** + * 数据集 未标注-->标注-->标注中 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationSaveEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dataMedicineMapper.updateStatus(medical.getId(), DcmDataStateEnum.ANNOTATION_DATA_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationDataState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + /** + * 数据集 未标注-->自动标注-->自动标注中 + * + * @param medical 医学数据集对象 + */ + @Override + public void autoAnnotationSaveEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dataMedicineMapper.updateStatus(medical.getId(), DcmDataStateEnum.AUTOMATIC_LABELING_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAutomaticLabelingDcmState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + + /** + * 数据集 未标注-->完成-->标注完成 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationCompleteEvent(DataMedicine medical) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件前内存中状态机的状态 :{} ", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", medical); + dataMedicineMapper.updateStatus(medical.getId(), DcmDataStateEnum.ANNOTATION_COMPLETE_STATE.getCode()); + dcmDataMedicineStateMachine.setMemoryDataMedicineState(dcmDataMedicineStateMachine.getAnnotationCompleteDcmState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件后内存状态机的切换: {}", dcmDataMedicineStateMachine.getMemoryDataMedicineState()); + } + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationCompleteDcmFileState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationCompleteDcmFileState.java new file mode 100644 index 0000000..c396f6a --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationCompleteDcmFileState.java @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.file; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineFileMapper; +import org.dubhe.dcm.machine.enums.DcmFileStateEnum; +import org.dubhe.dcm.machine.state.AbstractDcmFileState; +import org.dubhe.dcm.machine.statemachine.DcmFileStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.List; + + +/** + * @description 标注完成状态类 + * @date 2020-08-27 + */ +@Component +public class AnnotationCompleteDcmFileState extends AbstractDcmFileState { + + @Autowired + @Lazy + private DcmFileStateMachine dcmFileStateMachine; + + @Autowired + private DataMedicineFileMapper dataMedicineFileMapper; + + /** + * 文件事件 标注完成-->保存-->标注中 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationSaveEvent(List fileIds){ + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + + /** + * 文件 标注完成-->标注-->标注完成 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationCompleteEvent(List fileIds) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationCompleteDcmFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注完成】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationFileState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationFileState.java new file mode 100644 index 0000000..748e246 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AnnotationFileState.java @@ -0,0 +1,87 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.file; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineFileMapper; +import org.dubhe.dcm.machine.enums.DcmFileStateEnum; +import org.dubhe.dcm.machine.state.AbstractDcmFileState; +import org.dubhe.dcm.machine.statemachine.DcmFileStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @description 标注中 + * @date 2020-11-17 + */ +@Service +public class AnnotationFileState extends AbstractDcmFileState { + + @Autowired + @Lazy + private DcmFileStateMachine dcmFileStateMachine; + + @Autowired + private DataMedicineFileMapper dataMedicineFileMapper; + + /** + * 文件 标注中-->自动标注文件-->自动标注完成 + * + * @param fileIds 文件ID + */ + @Override + public void autoAnnotationEvent(List fileIds) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_COMPLETE_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationCompleteDcmFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + + /** + * 文件事件 标注中-->保存-->标注中 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationSaveEvent(List fileIds){ + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + + /** + * 文件 标注中-->标注-->标注完成 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationCompleteEvent(List fileIds) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_COMPLETE_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationCompleteDcmFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【标注中】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AutoAnnotationCompleteDcmFileState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AutoAnnotationCompleteDcmFileState.java new file mode 100644 index 0000000..6acdf12 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/AutoAnnotationCompleteDcmFileState.java @@ -0,0 +1,74 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.file; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineFileMapper; +import org.dubhe.dcm.machine.enums.DcmFileStateEnum; +import org.dubhe.dcm.machine.state.AbstractDcmFileState; +import org.dubhe.dcm.machine.statemachine.DcmFileStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.List; + + +/** + * @description 自动标注完成 + * @date 2020-08-27 + */ +@Component +public class AutoAnnotationCompleteDcmFileState extends AbstractDcmFileState { + + @Autowired + @Lazy + private DcmFileStateMachine dcmFileStateMachine; + + @Autowired + private DataMedicineFileMapper dataMedicineFileMapper; + + /** + * 文件 自动标注完成-->标注-->标注完成 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationCompleteEvent(List fileIds) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_COMPLETE_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationCompleteDcmFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + + /** + * 文件事件 自动标注完成-->保存-->标注中 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationSaveEvent(List fileIds){ + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【自动标注完成】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/NotAnnotationDcmFileState.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/NotAnnotationDcmFileState.java new file mode 100644 index 0000000..2f4943b --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/state/specific/file/NotAnnotationDcmFileState.java @@ -0,0 +1,102 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.state.specific.file; + + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.dcm.dao.DataMedicineFileMapper; +import org.dubhe.dcm.machine.enums.DcmFileStateEnum; +import org.dubhe.dcm.machine.state.AbstractDcmFileState; +import org.dubhe.dcm.machine.statemachine.DcmFileStateMachine; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.List; + + +/** + * @description 未标注状态类 + * @date 2020-08-27 + */ +@Component +public class NotAnnotationDcmFileState extends AbstractDcmFileState { + + @Autowired + @Lazy + private DcmFileStateMachine dcmFileStateMachine; + + @Autowired + private DataMedicineFileMapper dataMedicineFileMapper; + + /** + * 文件事件 未标注-->自动标注文件-->标注中 + * + * @param fileIds 文件ID + */ + @Override + public void annotationEvent(List fileIds) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAutoAnnotationCompleteDcmFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + /** + * 文件事件 未标注-->自动标注文件-->自动标注完成 + * + * @param fileIds 文件ID + */ + @Override + public void autoAnnotationSaveEvent(List fileIds) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.AUTO_ANNOTATION_COMPLETE_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAutoAnnotationCompleteDcmFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + + /** + * 文件事件 未标注-->保存-->标注中 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationSaveEvent(List fileIds){ + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + /** + * 文件 未标注-->标注-->标注完成 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationCompleteEvent(List fileIds) { + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件前内存中状态机的状态 :{} ", dcmFileStateMachine.getMemoryFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 接受参数: {} ", fileIds); + dataMedicineFileMapper.updateStatusByIds(fileIds, DcmFileStateEnum.ANNOTATION_COMPLETE_FILE_STATE.getCode()); + dcmFileStateMachine.setMemoryFileState(dcmFileStateMachine.getAnnotationCompleteDcmFileState()); + LogUtil.debug(LogEnum.STATE_MACHINE, " 【未标注】 执行事件后内存状态机的切换: {}", dcmFileStateMachine.getMemoryFileState()); + } + + +} \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmDataMedicineStateMachine.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmDataMedicineStateMachine.java new file mode 100644 index 0000000..b1c8d8e --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmDataMedicineStateMachine.java @@ -0,0 +1,196 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.statemachine; + +import lombok.Data; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.statemachine.exception.StateMachineException; +import org.dubhe.data.machine.constant.ErrorMessageConstant; +import org.dubhe.dcm.dao.DataMedicineMapper; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.machine.enums.DcmDataStateEnum; +import org.dubhe.dcm.machine.state.AbstractDataMedicineState; +import org.dubhe.dcm.machine.state.specific.datamedicine.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.Serializable; + +/** + * @description 数据状态机 + * @date 2020-08-27 + */ +@Data +@Component +public class DcmDataMedicineStateMachine extends AbstractDataMedicineState implements Serializable { + + /** + * 未标注 + */ + @Autowired + private NotAnnotationDcmState notAnnotationDcmState; + + /** + * 标注中 + */ + @Autowired + private AnnotationDataState annotationDataState; + + /** + * 自动标注中 + */ + @Autowired + private AutomaticLabelingDcmState automaticLabelingDcmState; + + /** + * 自动标注完成 + */ + @Autowired + private AutoAnnotationCompleteDcmState autoAnnotationCompleteDcmState; + + /** + * 标注完成 + */ + @Autowired + private AnnotationCompleteDcmState annotationCompleteDcmState; + + + /** + * 内存中的状态机 + */ + private AbstractDataMedicineState memoryDataMedicineState; + + @Autowired + private DataMedicineMapper dataMedicineMapper; + + + /** + * 初始化状态机的状态 + * + * @param primaryKeyId 业务ID + * @return dataMedicine 状态机实体 + */ + public DataMedicine initMemoryDataState(Long primaryKeyId) { + if (primaryKeyId == null) { + throw new StateMachineException("未找到业务ID"); + } + DataMedicine dataMedicine = dataMedicineMapper.selectById(primaryKeyId); + if (dataMedicine == null || dataMedicine.getStatus() == null) { + throw new StateMachineException("未找到业务数据"); + } + memoryDataMedicineState = SpringContextHolder.getBean(DcmDataStateEnum.getStateMachine(dataMedicine.getStatus())); + return dataMedicine; + } + + /** + * 初始化状态机的状态 + * + * @param medical 医学数据集对象 + * @return dataMedicine 状态机实体 + */ + public DataMedicine initMemoryDataState(DataMedicine medical) { + if (medical == null) { + throw new StateMachineException("医学影像服务状态机参数对象为空"); + } + if (medical.getStatus() == null) { + throw new StateMachineException("未找到业务数据"); + } + memoryDataMedicineState = SpringContextHolder.getBean(DcmDataStateEnum.getStateMachine(medical.getStatus())); + return medical; + } + + /** + * 标注事件 标注中/自动标注完成/完成/未标注-->保存-->标注中 + * + * @param medical 医学数据集对象 + */ + @Override + public void annotationSaveEvent(DataMedicine medical) { + initMemoryDataState(medical); + if (memoryDataMedicineState != notAnnotationDcmState && + memoryDataMedicineState != annotationDataState && + memoryDataMedicineState != autoAnnotationCompleteDcmState && + memoryDataMedicineState != annotationCompleteDcmState + ) { + + throw new StateMachineException(ErrorMessageConstant.DATASET_CHANGE_ERR_MESSAGE); + } + memoryDataMedicineState.annotationSaveEvent(medical); + } + /** + * 标注事件 未标注-->自动标注-->自动标注中 + * + * @param medical 业务对象 + */ + @Override + public void autoAnnotationSaveEvent(DataMedicine medical) { + initMemoryDataState(medical); + if (memoryDataMedicineState != notAnnotationDcmState) { + + throw new StateMachineException(ErrorMessageConstant.DATASET_CHANGE_ERR_MESSAGE); + } + memoryDataMedicineState.autoAnnotationSaveEvent(medical); + } + + /** + * 标注事件 注中/自动标注完成/完成/未标注-->完成-->标注完成 + * + * @param medical 业务对象 + */ + @Override + public void annotationCompleteEvent(DataMedicine medical) { + initMemoryDataState(medical.getId()); + if (memoryDataMedicineState != notAnnotationDcmState && + memoryDataMedicineState != annotationDataState && + memoryDataMedicineState != autoAnnotationCompleteDcmState && + memoryDataMedicineState != annotationCompleteDcmState + ) { + throw new StateMachineException(ErrorMessageConstant.DATASET_CHANGE_ERR_MESSAGE); + } + memoryDataMedicineState.annotationCompleteEvent(medical); + } + + /** + * 标注事件 标注中-->自动标注-->自动标注中 + * + * @param primaryKeyId 业务ID + */ + @Override + public void autoAnnotationEvent(Long primaryKeyId) { + initMemoryDataState(primaryKeyId); + if (memoryDataMedicineState != annotationDataState) { + throw new StateMachineException(ErrorMessageConstant.DATASET_CHANGE_ERR_MESSAGE); + } + memoryDataMedicineState.autoAnnotationEvent(primaryKeyId); + } + + /** + * 标注事件 自动标注中-->自动标注-->自动标注完成 + * + * @param primaryKeyId 业务ID + */ + @Override + public void autoAnnotationCompleteEvent(Long primaryKeyId) { + initMemoryDataState(primaryKeyId); + if (memoryDataMedicineState != automaticLabelingDcmState) { + throw new StateMachineException(ErrorMessageConstant.DATASET_CHANGE_ERR_MESSAGE); + } + memoryDataMedicineState.autoAnnotationCompleteEvent(primaryKeyId); + } + + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmFileStateMachine.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmFileStateMachine.java new file mode 100644 index 0000000..b384301 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmFileStateMachine.java @@ -0,0 +1,191 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.statemachine; + +import lombok.Data; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.utils.SpringContextHolder; +import org.dubhe.biz.statemachine.exception.StateMachineException; +import org.dubhe.data.machine.constant.ErrorMessageConstant; +import org.dubhe.dcm.dao.DataMedicineFileMapper; +import org.dubhe.dcm.domain.entity.DataMedicineFile; +import org.dubhe.dcm.machine.enums.DcmFileStateEnum; +import org.dubhe.dcm.machine.state.AbstractDcmFileState; +import org.dubhe.dcm.machine.state.specific.file.AnnotationCompleteDcmFileState; +import org.dubhe.dcm.machine.state.specific.file.AnnotationFileState; +import org.dubhe.dcm.machine.state.specific.file.AutoAnnotationCompleteDcmFileState; +import org.dubhe.dcm.machine.state.specific.file.NotAnnotationDcmFileState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @description 文件状态机 + * @date 2020-08-27 + */ +@Data +@Component +public class DcmFileStateMachine extends AbstractDcmFileState implements Serializable { + + @Autowired + private NotAnnotationDcmFileState notAnnotationFileState; + + @Autowired + private AnnotationFileState annotationFileState; + + @Autowired + private AutoAnnotationCompleteDcmFileState autoAnnotationCompleteDcmFileState; + + @Autowired + private AnnotationCompleteDcmFileState annotationCompleteDcmFileState; + + /** + * 内存中的状态机 + */ + private AbstractDcmFileState memoryFileState; + + + @Autowired + private DataMedicineFileMapper dataMedicineFileMapper; + + + /** + * 初始化状态机的状态 + * + * @param fileIds 文件ID列表 + */ + public void initMemoryFileState(List fileIds) { + if (CollectionUtils.isEmpty(fileIds)) { + throw new StateMachineException("未找到文件ID"); + } + List dataMedicineFileList = dataMedicineFileMapper.selectByIds(fileIds); + Map> dataMedicineFileMap = dataMedicineFileList.stream().collect(Collectors.groupingBy(DataMedicineFile::getStatus)); + if (dataMedicineFileMap.size() > MagicNumConstant.ONE) { + throw new StateMachineException(" 文件中存在状态不一致:【" + dataMedicineFileMap.entrySet() + "】"); + } + memoryFileState = SpringContextHolder.getBean(DcmFileStateEnum.getStateMachine(dataMedicineFileList.get(MagicNumConstant.ZERO).getStatus())); + } + + /** + * 初始化状态机的状态 + * + * @param fileIds 文件ID列表 + */ + public Map> getFileStateGroup(List fileIds) { + if (CollectionUtils.isEmpty(fileIds)) { + throw new StateMachineException("未找到文件ID"); + } + List dataMedicineFileList = dataMedicineFileMapper.selectByIds(fileIds); + return dataMedicineFileList.stream().collect(Collectors.groupingBy(DataMedicineFile::getStatus)); + } + + /** + * 文件事件 未标注-->自动标注文件-->标注中 + * + * @param fileIds 文件ID列表 + */ + @Override + public void annotationEvent(List fileIds) { + initMemoryFileState(fileIds); + if (memoryFileState != notAnnotationFileState) { + throw new StateMachineException(ErrorMessageConstant.FILE_CHANGE_ERR_MESSAGE); + } + memoryFileState.annotationEvent(fileIds); + } + + /** + * 文件事件 未标注-->自动标注文件-->标注中 + * + * @param fileIds 文件ID列表 + */ + @Override + public void autoAnnotationSaveEvent(List fileIds) { + initMemoryFileState(fileIds); + if (memoryFileState != notAnnotationFileState) { + throw new StateMachineException(ErrorMessageConstant.FILE_CHANGE_ERR_MESSAGE); + } + memoryFileState.autoAnnotationSaveEvent(fileIds); + } + + /** + * 文件事件 标注中/自动标注完成/完成/未标注-->保存-->标注中 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationSaveEvent(List fileIds) { + getFileStateGroup(fileIds).keySet().forEach(k -> { + memoryFileState = SpringContextHolder.getBean(DcmFileStateEnum.getStateMachine(k)); + if (memoryFileState != notAnnotationFileState && + memoryFileState != annotationFileState && + memoryFileState != autoAnnotationCompleteDcmFileState && + memoryFileState != annotationCompleteDcmFileState + ) { + throw new StateMachineException(ErrorMessageConstant.FILE_CHANGE_ERR_MESSAGE); + } + }); + getFileStateGroup(fileIds).keySet().forEach(k -> { + memoryFileState = SpringContextHolder.getBean(DcmFileStateEnum.getStateMachine(k)); + memoryFileState.annotationSaveEvent(fileIds); + }); + } + + + /** + * 文件事件 标注中/自动标注完成/完成/未标注-->完成-->标注完成 + * + * @param fileIds 医学数据集文件ID + */ + @Override + public void annotationCompleteEvent(List fileIds) { + getFileStateGroup(fileIds).keySet().forEach(k -> { + memoryFileState = SpringContextHolder.getBean(DcmFileStateEnum.getStateMachine(k)); + if (memoryFileState != notAnnotationFileState && + memoryFileState != annotationFileState && + memoryFileState != autoAnnotationCompleteDcmFileState && + memoryFileState != annotationCompleteDcmFileState + ) { + throw new StateMachineException(ErrorMessageConstant.FILE_CHANGE_ERR_MESSAGE); + } + }); + getFileStateGroup(fileIds).keySet().forEach(k -> { + memoryFileState = SpringContextHolder.getBean(DcmFileStateEnum.getStateMachine(k)); + memoryFileState.annotationCompleteEvent(fileIds); + }); + } + + /** + * 文件事件 标注中-->自动标注文件-->自动标注完成 + * + * @param fileIds 文件ID列表 + */ + @Override + public void autoAnnotationEvent(List fileIds) { + initMemoryFileState(fileIds); + if (memoryFileState != notAnnotationFileState) { + throw new StateMachineException(ErrorMessageConstant.FILE_CHANGE_ERR_MESSAGE); + } + memoryFileState.autoAnnotationEvent(fileIds); + } + + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmGlobalStateMachine.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmGlobalStateMachine.java new file mode 100644 index 0000000..6500532 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/statemachine/DcmGlobalStateMachine.java @@ -0,0 +1,40 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.statemachine; + +import lombok.Data; +import org.springframework.stereotype.Component; + +/** + * @description 全局状态机 + * @date 2020-08-27 + */ +@Data +@Component +public class DcmGlobalStateMachine { + + /** + * 数据状态机 + */ + private DcmDataMedicineStateMachine dcmDataMedicineStateMachine; + + /** + * 文件状态机 + */ + private DcmFileStateMachine dcmFileStateMachine; + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/utils/DcmStateMachineUtil.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/utils/DcmStateMachineUtil.java new file mode 100644 index 0000000..ce3c12d --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/machine/utils/DcmStateMachineUtil.java @@ -0,0 +1,49 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.machine.utils; + + +import org.dubhe.biz.statemachine.dto.StateChangeDTO; +import org.dubhe.dcm.machine.proxy.DcmStateMachineProxy; + +import java.util.List; + +/** + * @description 状态机工具类 业务层注入此类调用代理方法 + * @date 2020-08-27 + */ +public class DcmStateMachineUtil { + + /** + * 执行单个状态机的状态切换 + * + * @param stateChangeDTO 状态切换信息 + */ + public static void stateChange(StateChangeDTO stateChangeDTO) { + DcmStateMachineProxy.proxyExecutionSingleState(stateChangeDTO); + } + + /** + * 执行关联状态机的状态切换 + * + * @param stateChangeDTOList 状态切换信息 + */ + public static void stateChange(List stateChangeDTOList) { + DcmStateMachineProxy.proxyExecutionRelationState(stateChangeDTOList); + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataLesionSliceController.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataLesionSliceController.java new file mode 100644 index 0000000..d7f24f5 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataLesionSliceController.java @@ -0,0 +1,75 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.dcm.constant.DcmConstant; +import org.dubhe.dcm.domain.dto.DataLesionSliceCreateDTO; +import org.dubhe.dcm.domain.dto.DataLesionSliceDeleteDTO; +import org.dubhe.dcm.domain.dto.DataLesionSliceUpdateDTO; +import org.dubhe.dcm.service.DataLesionSliceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @description 病灶信息管理 + * @date 2020-12-23 + */ +@Api(tags = "医学数据处理:病灶信息管理") +@RestController +@RequestMapping(DcmConstant.MODULE_URL_PREFIX + "/datasets/medical/lesion") +public class DataLesionSliceController { + + @Autowired + private DataLesionSliceService dataLesionSliceService; + + @ApiOperation(value = "病灶信息保存") + @PostMapping("/{medicalId}") + @PreAuthorize(Permissions.DATA) + public DataResponseBody save(@Validated @RequestBody List dataLesionSliceCreateDTOS + , @PathVariable(name = "medicalId") Long medicineId) { + return new DataResponseBody(dataLesionSliceService.save(dataLesionSliceCreateDTOS,medicineId)); + } + + @ApiOperation(value = "病灶信息查询") + @GetMapping("/{medicalId}") + @PreAuthorize(Permissions.DATA) + public DataResponseBody query(@PathVariable(name = "medicalId") Long medicineId) { + return new DataResponseBody(dataLesionSliceService.get(medicineId)); + } + + @ApiOperation(value = "病灶信息删除") + @DeleteMapping + @PreAuthorize(Permissions.DATA) + public DataResponseBody delete(@Validated @RequestBody DataLesionSliceDeleteDTO dataLesionSliceDeleteDTO) { + return new DataResponseBody(dataLesionSliceService.delete(dataLesionSliceDeleteDTO)); + } + + @ApiOperation(value = "病灶信息修改") + @PutMapping + @PreAuthorize(Permissions.DATA) + public DataResponseBody update(@Validated @RequestBody DataLesionSliceUpdateDTO dataLesionSliceUpdateDTO) { + return new DataResponseBody(dataLesionSliceService.update(dataLesionSliceUpdateDTO)); + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataMedicineController.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataMedicineController.java new file mode 100644 index 0000000..93234ee --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/DataMedicineController.java @@ -0,0 +1,99 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.dcm.constant.DcmConstant; +import org.dubhe.dcm.domain.dto.*; +import org.dubhe.dcm.service.DataMedicineService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @description 医学数据集管理 + * @date 2020-11-11 + */ +@Api(tags = "医学数据处理:医学数据集管理") +@RestController +@RequestMapping(DcmConstant.MODULE_URL_PREFIX + "/datasets/medical") +public class DataMedicineController { + + @Autowired + private DataMedicineService dataMedicineService; + + @ApiOperation(value = "导入医学数据集") + @PostMapping(value = "/files") + @PreAuthorize(Permissions.DATA) + public DataResponseBody importDataMedicine(@Validated @RequestBody DataMedicineImportDTO dataMedicineImportDTO) { + return new DataResponseBody(dataMedicineService.importDataMedicine(dataMedicineImportDTO)); + } + + @ApiOperation(value = "医学数据集创建") + @PostMapping + @PreAuthorize(Permissions.DATA) + public DataResponseBody create(@Validated(DataMedicineCreateDTO.Create.class) @RequestBody DataMedicineCreateDTO dataMedicineCreateDTO) { + return new DataResponseBody(dataMedicineService.create(dataMedicineCreateDTO)); + } + + @ApiOperation(value = "医学数据集查询") + @GetMapping + @PreAuthorize(Permissions.DATA) + public DataResponseBody query(DataMedicineQueryDTO dataMedicineQueryDTO) { + return new DataResponseBody(dataMedicineService.listVO(dataMedicineQueryDTO)); + } + + @ApiOperation(value = "医学数据集删除") + @DeleteMapping + @PreAuthorize(Permissions.DATA) + public DataResponseBody delete(@Validated @RequestBody DataMedicineDeleteDTO dataMedicineDeleteDTO) { + return new DataResponseBody(dataMedicineService.delete(dataMedicineDeleteDTO)); + } + + @ApiOperation(value = "数据集修改") + @PutMapping(value = "/{medicalId}") + @PreAuthorize(Permissions.DATA) + public DataResponseBody update(@PathVariable(name = "medicalId") Long medicineId, + @Validated @RequestBody DataMedcineUpdateDTO dataMedcineUpdateDTO) { + return new DataResponseBody(dataMedicineService.update(dataMedcineUpdateDTO, medicineId)); + } + + @ApiOperation(value = "医学数据集详情") + @GetMapping("/detail/{medicalId}") + @PreAuthorize(Permissions.DATA) + public DataResponseBody get(@PathVariable(name = "medicalId") Long medicalId) { + return new DataResponseBody(dataMedicineService.get(medicalId)); + } + + @ApiOperation(value = "获取完成标注文件") + @GetMapping("/getFinished/{medicalId}") + @PreAuthorize(Permissions.DATA) + public DataResponseBody getFinished(@PathVariable(name = "medicalId") Long medicalId) { + return new DataResponseBody(dataMedicineService.getFinished(medicalId)); + } + + @ApiOperation(value = "获取自动标注文件") + @GetMapping("/getAuto/{medicalId}") + @PreAuthorize(Permissions.DATA) + public DataResponseBody getAuto(@PathVariable(name = "medicalId") Long medicalId) { + return new DataResponseBody(dataMedicineService.getAuto(medicalId)); + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/MedicineAnnotationController.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/MedicineAnnotationController.java new file mode 100644 index 0000000..b2ff56d --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/rest/MedicineAnnotationController.java @@ -0,0 +1,69 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.dubhe.biz.base.constant.Permissions; +import org.dubhe.biz.base.vo.DataResponseBody; +import org.dubhe.dcm.constant.DcmConstant; +import org.dubhe.dcm.domain.dto.MedicineAnnotationDTO; +import org.dubhe.dcm.domain.dto.MedicineAutoAnnotationDTO; +import org.dubhe.dcm.service.MedicineAnnotationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +/** + * @description 医学标注管理 + * @date 2020-11-16 + */ +@Api(tags = "医学数据处理:标注") +@RestController +@RequestMapping(DcmConstant.MODULE_URL_PREFIX + "/datasets/medical/annotation") +public class MedicineAnnotationController { + + @Autowired + private MedicineAnnotationService medicalAnnotationService; + + @ApiOperation(value = "医学数据自动标注") + @PostMapping("/auto") + @PreAuthorize(Permissions.DATA) + public DataResponseBody auto(@Validated @RequestBody MedicineAutoAnnotationDTO medicineAutoAnnotationDTO) { + medicalAnnotationService.auto(medicineAutoAnnotationDTO); + return new DataResponseBody(); + } + + @ApiOperation(value = "标注保存") + @PostMapping(value = "/save") + @PreAuthorize(Permissions.DATA) + public DataResponseBody save(@Validated @RequestBody MedicineAnnotationDTO medicineAnnotationDTO) { + return new DataResponseBody(medicalAnnotationService.save(medicineAnnotationDTO)); + } + + @ApiOperation(value = "标注进度") + @GetMapping(value = "/schedule") + @PreAuthorize(Permissions.DATA) + public DataResponseBody schedule(@RequestParam(value = "ids") List ids) { + return new DataResponseBody(medicalAnnotationService.schedule(ids)); + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataLesionSliceService.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataLesionSliceService.java new file mode 100644 index 0000000..235918b --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataLesionSliceService.java @@ -0,0 +1,81 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service; + +import org.dubhe.dcm.domain.dto.DataLesionSliceCreateDTO; +import org.dubhe.dcm.domain.dto.DataLesionSliceDeleteDTO; +import org.dubhe.dcm.domain.dto.DataLesionSliceUpdateDTO; +import org.dubhe.dcm.domain.entity.DataLesionSlice; +import org.dubhe.dcm.domain.vo.DataLesionSliceVO; + +import java.util.List; + +/** + * @description 病灶信息文件服务类 + * @date 2020-12-22 + */ +public interface DataLesionSliceService { + + /** + * 保存病灶信息 + * + * @param dataLesionSliceCreateDTOS 病灶层面信息列表 + * @param medicineId 数据集ID + * @return boolean 数据是否插入成功 + */ + boolean save(List dataLesionSliceCreateDTOS,Long medicineId); + + /** + * 批量插入病灶信息 + * + * @param dataLesionSliceList 病灶信息list + * @return boolean 数据是否插入成功 + */ + boolean insetDataLesionSliceBatch(List dataLesionSliceList); + + /** + * 获取病灶信息 + * + * @param medicineId 数据集ID + * @return List 病灶信息list + */ + List get(Long medicineId); + + /** + * 删除病灶信息 + * + * @param dataLesionSliceDeleteDTO 病灶信息删除DTO + * @return boolean 是否删除成功 + */ + boolean delete(DataLesionSliceDeleteDTO dataLesionSliceDeleteDTO); + + /** + * 修改病灶信息 + * + * @param dataLesionSliceUpdateDTO 病灶信息更新DTO + * @return boolean 是否修改成功 + */ + boolean update(DataLesionSliceUpdateDTO dataLesionSliceUpdateDTO); + + /** + * 保存时根据数据集Id清空病灶信息 + * + * @param medicineId 数据集ID + * @return boolean 是否删除成功 + */ + boolean deleteByMedicineId(Long medicineId); +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineFileService.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineFileService.java new file mode 100644 index 0000000..7cecf21 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineFileService.java @@ -0,0 +1,87 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.dubhe.dcm.domain.dto.DataMedicineFileCreateDTO; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.domain.entity.DataMedicineFile; + +import java.util.List; + +/** + * @description 医学数据集文件服务类 + * @date 2020-11-12 + */ +public interface DataMedicineFileService { + + /** + * 根据条件获取医学文件数量 + * + * @param queryWrapper 医学文件查询条件 + * @return 医学文件数量 + */ + Integer getCountByMedicineId(QueryWrapper queryWrapper); + + /** + * 插入医学数据集相关文件数据 + * + * @param dataMedicineFileList 文件路径 + * @param dataMedicine 医学数据集 + */ + void save(List dataMedicineFileList, DataMedicine dataMedicine); + + /** + * 获取医学文件列表 + * + * @param wrapper 查询条件 + * @return + */ + List listFile(QueryWrapper wrapper); + + + /** + * 补充文件详情后进行排序 + * + * @param dataMedicineFiles 医学文件列表 + * @param medicineId 医学数据集ID + * @return List 排序后的医学文件列表 + */ + List insertInstanceAndSort(List dataMedicineFiles,Long medicineId); + + /** + * 更新修改人ID + * + * @param medicineId 医学数据集id + */ + void updateUserIdByMedicineId(Long medicineId); + + /** + * 根据医学数据集Id修改文件状态 + * + * @param id 医学数据集Id + * @param deleteFlag 删除标识 + */ + void updateStatusById(Long id, Boolean deleteFlag); + + /** + * 根据医学数据集Id删除数据 + * + * @param datasetId 医学数据集Id + */ + void deleteByDatasetId(Long datasetId); +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineService.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineService.java new file mode 100644 index 0000000..411abbd --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/DataMedicineService.java @@ -0,0 +1,149 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service; + + +import com.alibaba.fastjson.JSONObject; +import org.dubhe.biz.base.enums.OperationTypeEnum; +import org.dubhe.dcm.domain.dto.*; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.domain.vo.DataMedicineCompleteAnnotationVO; +import org.dubhe.dcm.domain.vo.DataMedicineVO; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; + +import java.util.Map; + +/** + * @description 医学数据集服务类 + * @date 2020-11-11 + */ +public interface DataMedicineService { + + + /** + * 根据医学数据集ID获取数据集 + * + * @param medicineId 医学数据集ID + * @return 医学数据集实体 + */ + DataMedicine getDataMedicineById(Long medicineId); + + /** + * 导入医学数据集 + * + * @param dataMedicineImportDTO 导入医学数据集参数 + * @return boolean 导入是否成功 + */ + boolean importDataMedicine(DataMedicineImportDTO dataMedicineImportDTO); + + /** + * 创建医学数据集 + * + * @param dataMedicineCreateDTO 创建医学数据集参数 + * @return Long 医学数据集ID + */ + Long create(DataMedicineCreateDTO dataMedicineCreateDTO); + + /** + * 更新医学数据集 + * + * @param dataMedicine 医学数据集 + */ + void updateByMedicineId(DataMedicine dataMedicine); + + /** + * 删除数据集 + * + * @param dataMedicineDeleteDTO 删除数据集参数 + * @return boolean 是否删除成功 + */ + boolean delete(DataMedicineDeleteDTO dataMedicineDeleteDTO); + + /** + * 医学数据集查询 + * + * @param dataMedicineQueryDTO 查询条件 + * @return MapMap 查询出对应的数据集 + */ + Map listVO(DataMedicineQueryDTO dataMedicineQueryDTO); + + /** + * 医学数据集详情 + * + * @param medicalId 医学数据集ID + * @return DataMedicineVO 医学数据集VO + */ + DataMedicineVO get(Long medicalId); + + /** + * 根据医学数据集Id获取完成标注文件 + * + * @param medicalId 医学数据集ID + * @return JSONObject 标注文件 + */ + JSONObject getFinished(Long medicalId); + + /** + * 根据医学数据集Id获取自动标注文件 + * + * @param medicalId 医学数据集ID + * @return DataMedicineCompleteAnnotationVO 标注文件 + */ + DataMedicineCompleteAnnotationVO getAuto(Long medicalId); + + /** + * 根据医学数据集Id修改数据集 + * + * @param dataMedcineUpdateDTO 医学数据集修改DTO + * @param medicineId 医学数据集Id + * @return boolean 修改是否成功 + */ + boolean update(DataMedcineUpdateDTO dataMedcineUpdateDTO, Long medicineId); + + /** + * 检测是否为公共数据集 + * + * @param id 数据集ID + * @param type 校验类型 + * @return Boolean 更新结果 + */ + Boolean checkPublic(Long id, OperationTypeEnum type); + + /** + * 检测是否为公共数据集 + * + * @param dataMedicine 数据集 + * @param type 操作类型枚举 + * @return Boolean 更新结果 + */ + Boolean checkPublic(DataMedicine dataMedicine, OperationTypeEnum type); + + /** + * 数据集还原 + * + * @param dto 数据清理参数 + */ + void allRollback(RecycleCreateDTO dto); + + + /** + * 根据医学数据集Id删除数据 + * + * @param datasetId 医学数据集Id + */ + void deleteByDatasetId(Long datasetId); +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/MedicineAnnotationService.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/MedicineAnnotationService.java new file mode 100644 index 0000000..84c7a5e --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/MedicineAnnotationService.java @@ -0,0 +1,71 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service; + + +import org.dubhe.dcm.domain.dto.MedicineAnnotationDTO; +import org.dubhe.dcm.domain.dto.MedicineAutoAnnotationDTO; +import org.dubhe.dcm.domain.entity.DataMedicineFile; +import org.dubhe.dcm.domain.vo.ScheduleVO; + +import java.util.List; +import java.util.Map; + +/** + * @description 医学标注服务 + * @date 2020-11-16 + */ +public interface MedicineAnnotationService { + + /** + * 医学自动标注 + * + * @param medicineAutoAnnotationDTO 医学自动标注DTO + */ + void auto(MedicineAutoAnnotationDTO medicineAutoAnnotationDTO); + + /** + * 医学自动标注完成 + * + * @return boolean 是否有任务 + */ + boolean finishAuto(); + + /** + * 标注保存 + * + * @param medicineAnnotationDTO 医学标注DTO + * @return 保存是否成功 + */ + boolean save(MedicineAnnotationDTO medicineAnnotationDTO); + + /** + * 查询数据集标注的进度 + * + * @param ids 要查询的数据集ID + * @return 进度 + */ + Map schedule(List ids); + + /** + * 合并自动标注后的JSON文件 + * + * @param medicineId 医学数据集ID + * @param dataMedicineFiles 医学数据集文件列表 + */ + void mergeAnnotation(Long medicineId, List dataMedicineFiles); +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataLesionSliceServiceImpl.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataLesionSliceServiceImpl.java new file mode 100644 index 0000000..2542389 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataLesionSliceServiceImpl.java @@ -0,0 +1,163 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service.impl; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dubhe.biz.permission.annotation.DataPermissionMethod; +import org.dubhe.biz.base.enums.DatasetTypeEnum; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.dubhe.dcm.dao.DataLesionSliceMapper; +import org.dubhe.dcm.domain.dto.DataLesionSliceCreateDTO; +import org.dubhe.dcm.domain.dto.DataLesionSliceDeleteDTO; +import org.dubhe.dcm.domain.dto.DataLesionSliceUpdateDTO; +import org.dubhe.dcm.domain.entity.DataLesionSlice; +import org.dubhe.dcm.domain.vo.DataLesionSliceVO; +import org.dubhe.dcm.service.DataLesionSliceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + + + +/** + * @description 病灶信息服务实现类 + * @date 2020-12-22 + */ +@Service +public class DataLesionSliceServiceImpl extends ServiceImpl implements DataLesionSliceService { + + @Autowired + private DataLesionSliceMapper dataLesionSliceMapper; + + @Override + public boolean save(List dataLesionSliceCreateDTOS, Long medicineId) { + if (!CollectionUtils.isEmpty(dataLesionSliceCreateDTOS)) { + deleteByMedicineId(medicineId); + List dataLesionSliceList = new ArrayList<>(); + dataLesionSliceCreateDTOS.forEach(dataLesionSliceCreateDTO -> { + JSONArray jsonArray = new JSONArray(); + dataLesionSliceCreateDTO.getList().forEach(dataLesionDrawInfoDTO -> { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("drawId", dataLesionDrawInfoDTO.getDrawId()); + jsonObject.put("sliceNumber", dataLesionDrawInfoDTO.getSliceNumber()); + jsonArray.add(jsonObject); + }); + DataLesionSlice dataLesionSlice = new DataLesionSlice(dataLesionSliceCreateDTO.getLesionOrder() + , dataLesionSliceCreateDTO.getSliceDesc(), medicineId, jsonArray.toJSONString(), JwtUtils.getCurUserId()); + dataLesionSliceList.add(dataLesionSlice); + }); + insetDataLesionSliceBatch(dataLesionSliceList); + } else { + deleteByMedicineId(medicineId); + } + return true; + } + + /** + * 批量插入病灶信息 + * + * @param dataLesionSliceList 病灶信息list + * @return boolean 数据是否插入成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean insetDataLesionSliceBatch(List dataLesionSliceList) { + return saveBatch(dataLesionSliceList, dataLesionSliceList.size()); + } + + + /** + * 获取病灶信息 + * + * @param medicineId 数据集ID + * @return List 病灶信息list + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public List get(Long medicineId) { + QueryWrapper dataLesionSliceQueryWrapper = new QueryWrapper<>(); + dataLesionSliceQueryWrapper.lambda().eq(DataLesionSlice::getMedicineId, medicineId); + List dataLesionSlices = baseMapper.selectList(dataLesionSliceQueryWrapper); + List dataLesionSliceVOS = new ArrayList<>(); + dataLesionSlices.forEach(dataLesionSlice -> { + DataLesionSliceVO dataLesionSliceVO = new DataLesionSliceVO(); + dataLesionSliceVO.setId(dataLesionSlice.getId()); + dataLesionSliceVO.setLesionOrder(dataLesionSlice.getLesionOrder()); + dataLesionSliceVO.setSliceDesc(dataLesionSlice.getSliceDesc()); + dataLesionSliceVO.setList(dataLesionSlice.getDrawInfo()); + dataLesionSliceVOS.add(dataLesionSliceVO); + }); + return dataLesionSliceVOS; + } + + /** + * 删除病灶信息 + * + * @param dataLesionSliceDeleteDTO 病灶信息删除DTO + * @return boolean 是否删除成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean delete(DataLesionSliceDeleteDTO dataLesionSliceDeleteDTO) { + dataLesionSliceMapper.deleteByMedicineIdAndOrder(dataLesionSliceDeleteDTO.getId()); + return true; + } + + /** + * 修改病灶信息 + * + * @param dataLesionSliceUpdateDTO 病灶信息更新DTO + * @return boolean 是否修改成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean update(DataLesionSliceUpdateDTO dataLesionSliceUpdateDTO) { + DataLesionSlice dataLesionSlice = baseMapper.selectById(dataLesionSliceUpdateDTO.getId()); + dataLesionSlice.setLesionOrder(dataLesionSliceUpdateDTO.getLesionOrder()); + dataLesionSlice.setSliceDesc(dataLesionSliceUpdateDTO.getSliceDesc()); + JSONArray jsonArray = new JSONArray(); + dataLesionSliceUpdateDTO.getList().forEach(dataLesionDrawInfoDTO -> { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("drawId", dataLesionDrawInfoDTO.getDrawId()); + jsonObject.put("sliceNumber", dataLesionDrawInfoDTO.getSliceNumber()); + jsonArray.add(jsonObject); + }); + dataLesionSlice.setDrawInfo(jsonArray.toJSONString()); + baseMapper.updateById(dataLesionSlice); + return true; + } + + /** + * 保存时根据数据集Id清空病灶信息 + * + * @param medicineId 数据集ID + * @return boolean 是否删除成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteByMedicineId(Long medicineId) { + dataLesionSliceMapper.deleteByMedicineId(medicineId); + return true; + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineFileServiceImpl.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineFileServiceImpl.java new file mode 100644 index 0000000..26ab532 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineFileServiceImpl.java @@ -0,0 +1,202 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.lang3.StringUtils; +import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.Tag; +import org.dcm4che3.io.DicomInputStream; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.file.utils.MinioUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.dubhe.data.machine.constant.DataStateCodeConstant; +import org.dubhe.dcm.constant.DcmConstant; +import org.dubhe.dcm.dao.DataMedicineFileMapper; +import org.dubhe.dcm.domain.dto.DataMedicineFileCreateDTO; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.domain.entity.DataMedicineFile; +import org.dubhe.dcm.service.DataMedicineFileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * @description 医学数据集文件服务实现类 + * @date 2020-11-12 + */ +@Service +public class DataMedicineFileServiceImpl extends ServiceImpl implements DataMedicineFileService { + + @Autowired + private MinioUtil minioUtil; + + @Value("${minio.bucketName}") + private String bucketName; + + /** + * 插入医学数据集相关文件数据 + * + * @param dataMedicineFileCreateDTO 文件路径 + * @param dataMedicine 医学数据集 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void save(List dataMedicineFileCreateDTO, DataMedicine dataMedicine) { + List dataMedicineFiles = new ArrayList<>(); + for (DataMedicineFileCreateDTO dataMedicineFileCreate : dataMedicineFileCreateDTO) { + DataMedicineFile dataMedicineFile = new DataMedicineFile(); + String urlname = dataMedicineFileCreate.getUrl().substring(dataMedicineFileCreate.getUrl().lastIndexOf(DcmConstant.DCM_FILE_SEPARATOR) + MagicNumConstant.ONE); + String name = urlname.substring(MagicNumConstant.ZERO, urlname.indexOf(".")); + dataMedicineFile.setName(name); + dataMedicineFile.setStatus(DataStateCodeConstant.NOT_ANNOTATION_STATE); + dataMedicineFile.setMedicineId(dataMedicine.getId()); + dataMedicineFile.setUrl(dataMedicineFileCreate.getUrl()); + dataMedicineFile.setOriginUserId(dataMedicine.getCreateUserId()); + dataMedicineFile.setCreateUserId(dataMedicine.getCreateUserId()); + dataMedicineFile.setUpdateUserId(dataMedicine.getCreateUserId()); + dataMedicineFile.setSopInstanceUid(dataMedicineFileCreate.getSOPInstanceUID()); + dataMedicineFiles.add(dataMedicineFile); + } + baseMapper.saveBatch(dataMedicineFiles); + } + + /** + * 获取医学文件列表 + * + * @param wrapper 查询条件 + * @return List 医学文件列表 + */ + @Override + public List listFile(QueryWrapper wrapper) { + return list(wrapper); + } + + + /** + * 获取医学数据集文件数量 + * + * @param queryWrapper + * @return Integer 医学数据集文件数量 + */ + @Override + public Integer getCountByMedicineId(QueryWrapper queryWrapper) { + return baseMapper.selectCount(queryWrapper); + } + + /** + * 补充文件详情后进行排序 + * + * @param dataMedicineFiles 医学文件列表 + * @param medicineId 医学数据集ID + * @return List 排序后的医学文件列表 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public List insertInstanceAndSort(List dataMedicineFiles, Long medicineId) { + AtomicBoolean positionFlag = new AtomicBoolean(false); + dataMedicineFiles.forEach(dataMedicineFile -> { + Attributes attributes = null; + DicomInputStream dicomInputStream = null; + InputStream inputStream = null; + String targetPath = StringUtils.substringAfter(dataMedicineFile.getUrl(), "/"); + try { + inputStream = minioUtil.getObjectInputStream(bucketName, targetPath); + dicomInputStream = new DicomInputStream(inputStream); + attributes = dicomInputStream.readDataset(-1, -1); + int instanceNumber = Integer.parseInt(attributes.getString(Tag.InstanceNumber)); + String sopInstanceUid = attributes.getString(Tag.SOPInstanceUID); + if (attributes.getString(Tag.ImagePositionPatient) != null) { + String imagePositionPatientString = attributes.getString(Tag.ImagePositionPatient, 2); + double imagePositionPatient = Double.parseDouble(imagePositionPatientString); + dataMedicineFile.setImagePositionPatient(imagePositionPatient); + positionFlag.set(true); + } + dataMedicineFile.setInstanceNumber(instanceNumber); + dataMedicineFile.setSopInstanceUid(sopInstanceUid); + baseMapper.updateById(dataMedicineFile); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "get dicomInputStream failed, {}", e); + } finally { + try { + if(!Objects.isNull(dicomInputStream)){ + dicomInputStream.close(); + } + if(!Objects.isNull(inputStream)){ + inputStream.close(); + } + } catch (IOException e) { + LogUtil.error(LogEnum.BIZ_DATASET, "close inputStream failed, {}", e); + } + } + }); + QueryWrapper wrapper = new QueryWrapper<>(); + if (positionFlag.get()) { + wrapper.lambda().eq(DataMedicineFile::getMedicineId, medicineId) + .orderByAsc(DataMedicineFile::getImagePositionPatient); + } else { + wrapper.lambda().eq(DataMedicineFile::getMedicineId, medicineId) + .orderByAsc(DataMedicineFile::getInstanceNumber); + } + return listFile(wrapper); + } + + /** + * 更新修改人ID + * + * @param medicineId 医学数据集id + */ + @Override + public void updateUserIdByMedicineId(Long medicineId) { + baseMapper.updateUserIdByMedicineId(medicineId,JwtUtils.getCurUserId()); + } + + /** + * 根据医学数据集Id删除文件 + * + * @param id 医学数据集Id + * @param deleteFlag 删除标识 + */ + @Override + public void updateStatusById(Long id, Boolean deleteFlag) { + baseMapper.updateStatusById(id,deleteFlag); + } + + /** + * 根据医学数据集Id删除数据 + * + * @param datasetId 医学数据集Id + */ + @Override + public void deleteByDatasetId(Long datasetId) { + baseMapper.deleteByDatasetId(datasetId); + } + + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineServiceImpl.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineServiceImpl.java new file mode 100644 index 0000000..3a5ce1f --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/DataMedicineServiceImpl.java @@ -0,0 +1,537 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dubhe.biz.permission.annotation.DataPermissionMethod; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.context.DataContext; +import org.dubhe.biz.base.dto.CommonPermissionDataDTO; +import org.dubhe.biz.base.enums.DatasetTypeEnum; +import org.dubhe.biz.base.enums.OperationTypeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.permission.base.BaseService; +import org.dubhe.biz.db.utils.PageUtil; +import org.dubhe.biz.db.utils.WrapperHelp; +import org.dubhe.biz.file.utils.MinioUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.dubhe.data.constant.ErrorEnum; +import org.dubhe.data.machine.constant.DataStateCodeConstant; +import org.dubhe.dcm.constant.DcmConstant; +import org.dubhe.dcm.dao.DataMedicineMapper; +import org.dubhe.dcm.domain.dto.*; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.domain.vo.DataMedicineCompleteAnnotationVO; +import org.dubhe.dcm.domain.vo.DataMedicineVO; +import org.dubhe.dcm.service.DataMedicineFileService; +import org.dubhe.dcm.service.DataMedicineService; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.dto.RecycleDetailCreateDTO; +import org.dubhe.recycle.enums.RecycleModuleEnum; +import org.dubhe.recycle.enums.RecycleResourceEnum; +import org.dubhe.recycle.enums.RecycleTypeEnum; +import org.dubhe.recycle.service.RecycleService; +import org.dubhe.recycle.utils.RecycleTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.dubhe.data.constant.ErrorEnum.DATASET_PUBLIC_LIMIT_ERROR; + + +/** + * @description 医学数据集服务实现类 + * @date 2020-11-11 + */ +@Service +public class DataMedicineServiceImpl extends ServiceImpl implements DataMedicineService { + + /** + * 医学数据集文件服务 + */ + @Autowired + private DataMedicineFileService dataMedicineFileService; + + @Autowired + private DataMedicineMapper dataMedicineMapper; + + /** + * 数据回收服务 + */ + @Autowired + private RecycleService recycleService; + + @Autowired + private MinioUtil minioUtil; + + @Value("${minio.bucketName}") + private String bucketName; + + /** + * 路径名前缀 + */ + @Value("${storage.file-store-root-path:/nfs/}") + private String prefixPath; + + /** + * dcm服务器地址 + */ + @Value("${dcm.host}") + private String dcmHost; + + /** + * dcm服务端口 + */ + @Value("${dcm.port}") + private String dcmPort; + + /** + * nfs服务器地址 + */ + @Value("${storage.file-store}") + private String nfsHost; + + /** + * 文件服务器账号地址 需要免密 + */ + @Value("${data.server.userName}") + private String dataServerUserName; + + /** + * 获取DataMedicine中所有属性 + */ + private final Field[] fields = DataMedicine.class.getDeclaredFields(); + + /** + * 医学自动标注 + * + * @param medicineId 医学数据集ID + */ + + @Override + public DataMedicine getDataMedicineById(Long medicineId) { + return getById(medicineId); + } + + /** + * 检测是否为公共数据集 + * + * @param id 数据集id + * @return Boolean 是否为公共数据集 + */ + @Override + public Boolean checkPublic(Long id, OperationTypeEnum type) { + DataMedicine dataMedicine = baseMapper.selectById(id); + return checkPublic(dataMedicine, type); + } + + /** + * 检测是否为公共数据集 + * + * @param dataMedicine 数据集 + */ + @Override + public Boolean checkPublic(DataMedicine dataMedicine, OperationTypeEnum type) { + if (Objects.isNull(dataMedicine)) { + return false; + } + if (DatasetTypeEnum.PUBLIC.getValue().equals(dataMedicine.getType())) { + //操作类型校验公共数据集 + if (OperationTypeEnum.UPDATE.equals(type)) { + BaseService.checkAdminPermission(); + //操作类型校验公共数据集 + } else if (OperationTypeEnum.LIMIT.equals(type)) { + throw new BusinessException(DATASET_PUBLIC_LIMIT_ERROR); + } else { + return true; + } + + } + return false; + } + + + /** + * 数据还原 + * + * @param dto 数据清理参数 + */ + @Override + public void allRollback(RecycleCreateDTO dto) { + List detailList = dto.getDetailList(); + if (CollectionUtil.isNotEmpty(detailList)) { + for (RecycleDetailCreateDTO recycleDetailCreateDTO : detailList) { + if (!Objects.isNull(recycleDetailCreateDTO) && + RecycleTypeEnum.TABLE_DATA.getCode().compareTo(recycleDetailCreateDTO.getRecycleType()) == 0) { + Long datasetId = Long.valueOf(recycleDetailCreateDTO.getRecycleCondition()); + DataMedicine dataMedicine = baseMapper.findDataMedicineByIdAndDeleteIsFalse(datasetId); + DataMedicine dataMedicineBySeriesUid = baseMapper.findDataMedicineBySeriesUid(dataMedicine.getSeriesInstanceUid()); + if(dataMedicineBySeriesUid != null){ + throw new BusinessException(ErrorEnum.MEDICINE_MEDICAL_ALREADY_EXISTS_RESTORE); + } + //还原数据集状态 + baseMapper.updateStatusById(datasetId, false); + //还原数据集文件状态 + dataMedicineFileService.updateStatusById(datasetId, false); + return; + } + } + + } + } + + + /** + * 根据医学数据集Id删除数据 + * + * @param datasetId 医学数据集Id + */ + @Override + public void deleteByDatasetId(Long datasetId) { + baseMapper.deleteByDatasetId(datasetId); + } + + /** + * 导入医学数据集 + * + * @param dataMedicineImportDTO 导入医学数据集参数 + * @return boolean 导入是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean importDataMedicine(DataMedicineImportDTO dataMedicineImportDTO) { + DataMedicine dataMedicineUpdate = baseMapper.selectById(dataMedicineImportDTO.getId()); + baseMapper.updateById(dataMedicineUpdate); + dataMedicineFileService.save(dataMedicineImportDTO.getDataMedicineFileCreateList(), dataMedicineUpdate); + //上传dcm文件到dcm服务器 + String command = String.format(DcmConstant.DCM_UPLOAD, dataServerUserName, nfsHost, prefixPath, dcmHost, dcmPort, + prefixPath + File.separator + bucketName + File.separator + "dataset" + File.separator + "dcm" + File.separator + dataMedicineImportDTO.getId() + File.separator + "origin"); + try { + Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", command}); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "org.dubhe.dcm file upload fail"); + throw new BusinessException("dcm文件上传失败"); + } + return true; + } + + /** + * 创建医学数据集 + * + * @param dataMedicineCreateDTO 创建医学数据集参数 + * @return Long 医学数据集ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(DataMedicineCreateDTO dataMedicineCreateDTO) { + Long originUserId = JwtUtils.getCurUserId(); + DataMedicine dataMedicine = dataMedicineMapper.findBySeriesUidAndNotId(dataMedicineCreateDTO.getSeriesInstanceUID(), originUserId); + if (dataMedicine != null) { + throw new BusinessException(ErrorEnum.MEDICINE_MEDICAL_ALREADY_EXISTS); + } + QueryWrapper dataMedicineQueryWrapper = new QueryWrapper<>(); + dataMedicineQueryWrapper.eq("name", dataMedicineCreateDTO.getName()); + int count = baseMapper.selectCount(dataMedicineQueryWrapper); + if (count > MagicNumConstant.ZERO) { + throw new BusinessException(ErrorEnum.MEDICINE_NAME_ERROR); + } + + DataMedicine dataMedicineCreate = DataMedicineCreateDTO.from(dataMedicineCreateDTO, JwtUtils.getCurUserId()); + save(dataMedicineCreate); + return dataMedicineCreate.getId(); + } + + /** + * 更新医学数据集 + * + * @param dataMedicine 医学数据集 + */ + @Override + public void updateByMedicineId(DataMedicine dataMedicine) { + baseMapper.updateById(dataMedicine); + } + + /** + * 删除数据集 + * + * @param dataMedicineDeleteDTO 删除数据集参数 + * @return boolean 是否删除成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean delete(DataMedicineDeleteDTO dataMedicineDeleteDTO) { + for (Long id : dataMedicineDeleteDTO.getIds()) { + deleteDataMedicine(id); + } + return true; + } + + /** + * 删除数据集 + * + * @return boolean 是否删除成功 + */ + @Transactional(rollbackFor = Exception.class) + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public boolean deleteDataMedicine(Long id) { + DataMedicine dataMedicine = baseMapper.selectById(id); + if (dataMedicine == null) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_ABSENT); + } + checkPublic(dataMedicine, OperationTypeEnum.UPDATE); + if (dataMedicine.getStatus().equals(DataStateCodeConstant.AUTOMATIC_LABELING_STATE)) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_AUTOMATIC); + } + baseMapper.updateStatusById(id, true); + dataMedicineFileService.updateStatusById(id, true); + addRecycleDataByDeleteDataset(id); + return true; + } + + /** + * 添加医学数据集删除回收数据 + * + * @param id 医学数据集ID + */ + @Transactional(rollbackFor = Exception.class) + public void addRecycleDataByDeleteDataset(Long id) { + //删除MinIO文件 + try { + //落地回收详情数据文件回收信息 + List detailList = new ArrayList<>(); + detailList.add(RecycleDetailCreateDTO.builder() + .recycleCondition(id.toString()) + .recycleType(RecycleTypeEnum.TABLE_DATA.getCode()) + .recycleNote(RecycleTool.generateRecycleNote("落地 数据集DB 数据文件回收", id)) + .build()); + //落地回收详情minio 数据文件回收信息 + detailList.add(RecycleDetailCreateDTO.builder() + .recycleCondition(prefixPath + bucketName + DcmConstant.DCM_FILE_SEPARATOR + DcmConstant.DCM_ANNOTATION_PATH + id) + .recycleType(RecycleTypeEnum.FILE.getCode()) + .recycleNote(RecycleTool.generateRecycleNote("落地 minio 数据文件回收", id)) + .build()); + + //落地回收信息 + RecycleCreateDTO recycleCreateDTO = RecycleCreateDTO.builder() + .recycleModule(RecycleModuleEnum.BIZ_DATAMEDICINE.getValue()) + .recycleCustom(RecycleResourceEnum.DATAMEDICINE_RECYCLE_FILE.getClassName()) + .restoreCustom(RecycleResourceEnum.DATAMEDICINE_RECYCLE_FILE.getClassName()) + .recycleDelayDate(NumberConstant.NUMBER_1) + .recycleNote(RecycleTool.generateRecycleNote("删除医学数据集相关信息", id)) + .detailList(detailList) + .build(); + recycleService.createRecycleTask(recycleCreateDTO); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "DataMedicineServiceImpl addRecycleDataByDeleteDataset error {}", e); + } + + } + + /** + * 医学数据集查询 + * + * @param dataMedicineQueryDTO 查询条件 + * @return MapMap 查询出对应的数据集 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public Map listVO(DataMedicineQueryDTO dataMedicineQueryDTO) { + if (dataMedicineQueryDTO.getCurrent() == null || dataMedicineQueryDTO.getSize() == null) { + throw new BusinessException(ErrorEnum.PARAM_ERROR); + } + QueryWrapper wrapper = WrapperHelp.getWrapper(dataMedicineQueryDTO); + if (dataMedicineQueryDTO.getAnnotateType() != null) { + if (dataMedicineQueryDTO.getAnnotateType() % MagicNumConstant.ONE_THOUSAND == MagicNumConstant.ZERO) { + wrapper.between("annotate_type", dataMedicineQueryDTO.getAnnotateType() + , dataMedicineQueryDTO.getAnnotateType() + MagicNumConstant.ONE_THOUSAND); + } else { + wrapper.eq("annotate_type", dataMedicineQueryDTO.getAnnotateType()); + } + } + wrapper.eq("deleted", MagicNumConstant.ZERO) + .eq("type", dataMedicineQueryDTO.getType()); + //预置数据集类型校验 + if (!Objects.isNull(dataMedicineQueryDTO.getType()) && dataMedicineQueryDTO.getType().compareTo(DatasetTypeEnum.PUBLIC.getValue()) == 0) { + DataContext.set(CommonPermissionDataDTO.builder().id(null).type(true).build()); + } + if (StringUtils.isNotBlank(dataMedicineQueryDTO.getName())) { + wrapper.lambda().and(w -> + w.eq(DataMedicine::getId, dataMedicineQueryDTO.getName()) + .or() + .like(DataMedicine::getName, dataMedicineQueryDTO.getName()) + ); + } + if (StringUtils.isNotBlank(dataMedicineQueryDTO.getSort())) { + for (Field field : fields) { + if (field.getName().equals(dataMedicineQueryDTO.getSort())) { + field.setAccessible(true); + TableField annotation = field.getAnnotation(TableField.class); + if (annotation == null) { + continue; + } + if ("desc".equals(dataMedicineQueryDTO.getOrder())) { + wrapper.orderByDesc(annotation.value()); + } else if ("asc".equals(dataMedicineQueryDTO.getOrder())) { + wrapper.orderByAsc(annotation.value()); + } + } + } + } else { + wrapper.lambda().orderByDesc(DataMedicine::getUpdateTime); + } + Page pages = new Page() {{ + setCurrent(dataMedicineQueryDTO.getCurrent()); + setSize(dataMedicineQueryDTO.getSize()); + setTotal(baseMapper.selectCount(wrapper)); + List collect = baseMapper.selectList( + wrapper + .last(" limit " + (dataMedicineQueryDTO.getCurrent() - NumberConstant.NUMBER_1) * dataMedicineQueryDTO.getSize() + ", " + dataMedicineQueryDTO.getSize()) + ).stream().map(DataMedicineVO::from).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(collect)) { + setRecords(collect); + } + }}; + return PageUtil.toPage(pages); + } + + /** + * 医学数据集详情 + * + * @param medicalId 医学数据集ID + * @return DataMedicineVO 医学数据集VO + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public DataMedicineVO get(Long medicalId) { + DataMedicine dataMedicine = baseMapper.selectById(medicalId); + if (dataMedicine == null) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_ABSENT); + } else if (dataMedicine.getStatus().equals(DataStateCodeConstant.AUTOMATIC_LABELING_STATE)) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_AUTOMATIC); + } + if (checkPublic(medicalId, OperationTypeEnum.SELECT)) { + DataContext.set(CommonPermissionDataDTO.builder().id(medicalId).type(true).build()); + } + DataMedicineVO dataMedicineVO = DataMedicineVO.from(dataMedicine); + return dataMedicineVO; + } + + /** + * 根据医学数据集Id获取完成标注文件 + * + * @param medicalId 医学数据集ID + * @return JSONObject 完成标注文件 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public JSONObject getFinished(Long medicalId) { + DataMedicine dataMedicine = baseMapper.selectById(medicalId); + if (dataMedicine == null) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_ABSENT); + } + try { + String finishedFilePath = DcmConstant.DCM_ANNOTATION_PATH + medicalId + DcmConstant.DCM_ANNOTATION; + String annotation = minioUtil.readString(bucketName, finishedFilePath); + JSONObject jsonObject = JSONObject.parseObject(annotation); + return jsonObject; + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "MinIO read the dataMedicine file error", e); + } + return null; + } + + /** + * 根据医学数据集Id获取自动标注文件 + * + * @param medicalId 医学数据集ID + * @return DataMedicineCompleteAnnotationVO 标注文件 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public DataMedicineCompleteAnnotationVO getAuto(Long medicalId) { + DataMedicine dataMedicine = baseMapper.selectById(medicalId); + if (dataMedicine == null) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_ABSENT); + } + DataMedicineCompleteAnnotationVO dataMedicineCompleteAnnotationVO = new DataMedicineCompleteAnnotationVO(); + try { + String autoFilePath = DcmConstant.DCM_ANNOTATION_PATH + medicalId + DcmConstant.DCM_MERGE_ANNOTATION; + String annotation = minioUtil.readString(bucketName, autoFilePath); + JSONObject jsonObject = JSONObject.parseObject(annotation); + dataMedicineCompleteAnnotationVO.setSeriesInstanceUID(jsonObject.getString(DcmConstant.SERIES_INSTABCE_UID)); + dataMedicineCompleteAnnotationVO.setStudyInstanceUID(jsonObject.getString(DcmConstant.STUDY_INSTANCE_UID)); + dataMedicineCompleteAnnotationVO.setAnnotations(jsonObject.getString(DcmConstant.ANNOTATION)); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "MinIO read the dataMedicine file error {}", e); + } + return dataMedicineCompleteAnnotationVO; + } + + /** + * 根据医学数据集Id修改数据集 + * + * @param dataMedcineUpdateDTO 医学数据集修改DTO + * @param medicineId 医学数据集Id + * @return boolean 修改是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public boolean update(DataMedcineUpdateDTO dataMedcineUpdateDTO, Long medicineId) { + DataMedicine dataMedicine = baseMapper.selectById(medicineId); + if (dataMedicine == null) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_ABSENT); + } + checkPublic(dataMedicine, OperationTypeEnum.UPDATE); + dataMedicine.setName(dataMedcineUpdateDTO.getName()); + dataMedicine.setUpdateUserId(JwtUtils.getCurUserId()); + if (dataMedcineUpdateDTO.getRemark() != null) { + dataMedicine.setRemark(dataMedcineUpdateDTO.getRemark()); + } + + int count; + try { + count = baseMapper.updateById(dataMedicine); + } catch (DuplicateKeyException e) { + throw new BusinessException(ErrorEnum.DATASET_NAME_DUPLICATED_ERROR, null, e); + } + if (count == MagicNumConstant.ZERO) { + throw new BusinessException(ErrorEnum.DATA_ABSENT_OR_NO_AUTH); + } + return true; + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/MedicineAnnotationServiceImpl.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/MedicineAnnotationServiceImpl.java new file mode 100644 index 0000000..ddf494b --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/impl/MedicineAnnotationServiceImpl.java @@ -0,0 +1,380 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.dubhe.biz.permission.annotation.DataPermissionMethod; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.enums.DatasetTypeEnum; +import org.dubhe.biz.base.enums.OperationTypeEnum; +import org.dubhe.biz.base.exception.BusinessException; +import org.dubhe.biz.file.utils.MinioUtil; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.redis.utils.RedisUtils; +import org.dubhe.biz.statemachine.dto.StateChangeDTO; +import org.dubhe.cloud.authconfig.utils.JwtUtils; +import org.dubhe.data.constant.DataTaskTypeEnum; +import org.dubhe.data.constant.ErrorEnum; +import org.dubhe.data.constant.TaskStatusEnum; +import org.dubhe.data.domain.entity.Task; +import org.dubhe.data.machine.constant.DataStateCodeConstant; +import org.dubhe.data.machine.enums.DataStateEnum; +import org.dubhe.data.service.TaskService; +import org.dubhe.dcm.constant.DcmConstant; +import org.dubhe.dcm.dao.DataMedicineFileMapper; +import org.dubhe.dcm.domain.dto.MedicineAnnotationDTO; +import org.dubhe.dcm.domain.dto.MedicineAutoAnnotationDTO; +import org.dubhe.dcm.domain.entity.DataMedicine; +import org.dubhe.dcm.domain.entity.DataMedicineFile; +import org.dubhe.dcm.domain.vo.ScheduleVO; +import org.dubhe.dcm.machine.constant.DcmDataStateMachineConstant; +import org.dubhe.dcm.machine.constant.DcmFileStateCodeConstant; +import org.dubhe.dcm.machine.constant.DcmFileStateMachineConstant; +import org.dubhe.dcm.machine.enums.DcmDataStateEnum; +import org.dubhe.dcm.machine.utils.DcmStateMachineUtil; +import org.dubhe.dcm.service.DataLesionSliceService; +import org.dubhe.dcm.service.DataMedicineFileService; +import org.dubhe.dcm.service.DataMedicineService; +import org.dubhe.dcm.service.MedicineAnnotationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.stream.Collectors; + + +/** + * @description 医学标注服务实现类 + * @date 2020-11-16 + */ +@Service +public class MedicineAnnotationServiceImpl implements MedicineAnnotationService { + + /** + * 任务服务 + */ + @Autowired + private TaskService taskService; + + @Autowired + private DataMedicineFileMapper dataMedicineFileMapper; + + /** + * 医学数据集服务 + */ + @Autowired + private DataMedicineService dataMedicineService; + + /** + * 医学数据集文件服务 + */ + @Autowired + private DataMedicineFileService dataMedicineFileService; + + @Autowired + private MinioUtil minioUtil; + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private DataLesionSliceService dataLesionSliceService; + + /** + * bucketName + */ + @Value("${minio.bucketName}") + private String bucketName; + + + /** + * 医学标注算法处理中任务队列 + */ + private static final String MEDICINE_START_QUEUE = "dcm_processing_queue"; + /** + * 医学标注算法已完成任务队列 + */ + private static final String MEDICINE_FINISHED_QUEUE = "dcm_finished_queue"; + + /** + * 医学自动标注 + * + * @param medicineAutoAnnotationDTO 医学自动标注DTO + */ + @Override + @Transactional(rollbackFor = Exception.class) + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public void auto(MedicineAutoAnnotationDTO medicineAutoAnnotationDTO) { + if (medicineAutoAnnotationDTO.getMedicalId() == null) { + return; + } + dataMedicineService.checkPublic(medicineAutoAnnotationDTO.getMedicalId(), OperationTypeEnum.UPDATE); + Long medicineId = medicineAutoAnnotationDTO.getMedicalId(); + DataMedicine dataMedicine = dataMedicineService.getDataMedicineById(medicineId); + if (!dataMedicine.getStatus().equals(DataStateCodeConstant.NOT_ANNOTATION_STATE)) { + throw new BusinessException(ErrorEnum.MEDICINE_AUTO_DATASET_ERROR); + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(DataMedicineFile::getMedicineId, medicineId).eq(DataMedicineFile::getStatus, + DataStateEnum.NOT_ANNOTATION_STATE.getCode()); + Integer medicineFilesCount = dataMedicineFileService.getCountByMedicineId(queryWrapper); + Task task = Task.builder() + .status(TaskStatusEnum.INIT.getValue()) + .datasetId(medicineId) + .total(medicineFilesCount) + .type(DataTaskTypeEnum.MEDICINE_ANNOTATION.getValue()) + .labels("") + .build(); + taskService.createTask(task); + DcmStateMachineUtil.stateChange(new StateChangeDTO() {{ + setObjectParam(new Object[]{dataMedicine}); + setStateMachineType(DcmDataStateMachineConstant.DCM_DATA_STATE_MACHINE); + setEventMethodName(DcmDataStateMachineConstant.AUTO_ANNOTATION_SAVE_EVENT); + }}); + modifyUpdataUserId(medicineAutoAnnotationDTO.getMedicalId()); + } + + /** + * 医学自动标注完成 + * + * @return boolean 是否有任务 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean finishAuto() { + Object object = redisUtils.lpop(MEDICINE_FINISHED_QUEUE); + if (ObjectUtil.isNotNull(object)) { + JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(redisUtils.get(object.toString()))); + String detailId = jsonObject.getString("reTaskId"); + JSONObject jsonDetail = JSON.parseObject(JSON.toJSONString(redisUtils.get(detailId))); + Long taskId = jsonDetail.getLong("taskId"); + JSONArray dcmsArray = jsonDetail.getJSONArray("dcms"); + String[] dcms = dcmsArray.toArray(new String[dcmsArray.size()]); + QueryWrapper taskQueryWrapper = new QueryWrapper<>(); + taskQueryWrapper.lambda().eq(Task::getId, taskId); + Task task = taskService.selectOne(taskQueryWrapper); + List medicineFileIds = JSON.parseObject(jsonDetail.getString("medicineFileIds"), ArrayList.class); + DcmStateMachineUtil.stateChange(new StateChangeDTO() {{ + setObjectParam(new Object[]{medicineFileIds}); + setStateMachineType(DcmFileStateMachineConstant.DCM_FILE_STATE_MACHINE); + setEventMethodName(DcmFileStateMachineConstant.AUTO_ANNOTATION_SAVE_EVENT); + }}); + int finished = task.getFinished() + dcmsArray.size(); + if (task.getFinished() + dcmsArray.size() >= task.getTotal()) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(DataMedicineFile::getMedicineId, task.getDatasetId()); + List dataMedicineFiles = dataMedicineFileService.listFile(wrapper); + List dataMedicineFilesOrderByAsc = dataMedicineFileService + .insertInstanceAndSort(dataMedicineFiles, task.getDatasetId()); + mergeAnnotation(task.getDatasetId(), dataMedicineFilesOrderByAsc); + DataMedicine dataMedicine = dataMedicineService.getDataMedicineById(task.getDatasetId()); + DcmStateMachineUtil.stateChange(new StateChangeDTO() {{ + setObjectParam(new Object[]{dataMedicine.getId()}); + setStateMachineType(DcmDataStateMachineConstant.DCM_DATA_STATE_MACHINE); + setEventMethodName(DcmDataStateMachineConstant.AUTO_ANNOTATION_COMPLETE_EVENT); + }}); + } + task.setFinished(finished); + taskService.updateByTaskId(task); + } + return ObjectUtil.isNotNull(object); + } + + /** + * 标注保存 + * + * @param medicineAnnotationDTO 医学标注DTO + * @return 保存是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public boolean save(MedicineAnnotationDTO medicineAnnotationDTO) { + //判断保存时是否传fileID + if (medicineAnnotationDTO.getType().equals(NumberConstant.NUMBER_0)&&CollectionUtils.isEmpty(medicineAnnotationDTO.getMedicalFiles())){ + return true; + } + //判断是否为空,是否删除,是否是自动标注中状态 + DataMedicine medical = dataMedicineService.getDataMedicineById(medicineAnnotationDTO.getMedicalId()); + if (medical == null || medical.getDeleted()) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_ABSENT); + } else if (DcmDataStateEnum.AUTOMATIC_LABELING_STATE.getCode().equals(medical.getStatus())) { + throw new BusinessException(ErrorEnum.DATAMEDICINE_AUTOMATIC); + } + dataMedicineService.checkPublic(medicineAnnotationDTO.getMedicalId(),OperationTypeEnum.UPDATE); + //保存标注 + try { + minioUtil.writeString(bucketName, DcmConstant.DCM_ANNOTATION_PATH + medical.getId() + DcmConstant.DCM_ANNOTATION, medicineAnnotationDTO.getAnnotations()); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "Medicine annotation is failed" + e.getMessage()); + return false; + } + //改变数据集的状态为标注完成或标注中 + DcmStateMachineUtil.stateChange(new StateChangeDTO() {{ + setObjectParam(new Object[]{medical}); + setStateMachineType(DcmDataStateMachineConstant.DCM_DATA_STATE_MACHINE); + setEventMethodName(NumberConstant.NUMBER_1 == medicineAnnotationDTO.getType() ? + DcmDataStateMachineConstant.ANNOTATION_COMPLETE_EVENT : DcmDataStateMachineConstant.ANNOTATION_SAVE_EVENT); + }}); + //改变数据集文件的状态为标注完成或标注中 + DcmStateMachineUtil.stateChange(new StateChangeDTO() {{ + setObjectParam(new Object[]{ + NumberConstant.NUMBER_1 == medicineAnnotationDTO.getType() ? + dataMedicineFileMapper.selectList(new LambdaQueryWrapper() {{ + eq(DataMedicineFile::getMedicineId, medical.getId()); + }}).stream().map(DataMedicineFile::getId).collect(Collectors.toList()) : + dataMedicineFileMapper.selectList(new LambdaQueryWrapper() {{ + in(DataMedicineFile::getSopInstanceUid, medicineAnnotationDTO.getMedicalFiles()); + }}).stream().map(DataMedicineFile::getId).collect(Collectors.toList()) + }); + setStateMachineType(DcmFileStateMachineConstant.DCM_FILE_STATE_MACHINE); + setEventMethodName(NumberConstant.NUMBER_1 == medicineAnnotationDTO.getType() ? + DcmFileStateMachineConstant.ANNOTATION_COMPLETE_EVENT : DcmFileStateMachineConstant.ANNOTATION_SAVE_EVENT); + }}); + modifyUpdataUserId(medicineAnnotationDTO.getMedicalId()); + return true; + } + + /** + * 查询数据集标注的进度 + * + * @param ids 要查询的数据集ID + * @return 进度 + */ + @Override + @DataPermissionMethod(dataType = DatasetTypeEnum.PUBLIC) + public Map schedule(List ids) { + if (ids.isEmpty()) { + throw new BusinessException(ErrorEnum.PARAM_ERROR); + } + List> fileStatusCount = dataMedicineFileMapper.getFileStatusCount(ids); + if (fileStatusCount.isEmpty()){ + return new HashMap(ids.size()) {{ + ids.forEach(v -> { + ScheduleVO scheduleVO = new ScheduleVO(NumberConstant.NUMBER_0, NumberConstant.NUMBER_0, NumberConstant.NUMBER_0, NumberConstant.NUMBER_0); + //初始化进度值 + put(String.valueOf(v), scheduleVO); + }); + + }}; + } + return new HashMap(ids.size()) {{ + ids.forEach(v -> { + ScheduleVO scheduleVO = new ScheduleVO(NumberConstant.NUMBER_0, NumberConstant.NUMBER_0, NumberConstant.NUMBER_0, NumberConstant.NUMBER_0); + //初始化进度值 + put(String.valueOf(v), scheduleVO); + fileStatusCount.forEach(val -> { + if (v.equals(val.get(DcmConstant.MEDICINE_ID)) || v.equals(val.get(DcmConstant.MEDICINE_ID.toLowerCase()))) { + Integer count = Integer.valueOf(val.get(DcmConstant.COUNT).toString()); + Object status = val.get(DcmConstant.STATUS); + if (status.equals(DcmFileStateCodeConstant.NOT_ANNOTATION_FILE_STATE)) { + scheduleVO.setUnfinished(count); + } else if (status.equals(DcmFileStateCodeConstant.ANNOTATION_COMPLETE_FILE_STATE)) { + scheduleVO.setFinished(count); + } else if (status.equals(DcmFileStateCodeConstant.AUTO_ANNOTATION_COMPLETE_FILE_STATE)) { + scheduleVO.setAutoFinished(count); + } else if (status.equals(DcmFileStateCodeConstant.ANNOTATION_FILE_STATE)) { + scheduleVO.setManualAnnotating(count); + } + } + }); + }); + + }}; + } + + /** + * 合并自动标注后的JSON文件 + * + * @param medicineId 医学数据集ID + * @param dataMedicineFiles 医学数据集文件列表 + */ + @Override + public void mergeAnnotation(Long medicineId, List dataMedicineFiles) { + DataMedicine dataMedicine = dataMedicineService.getDataMedicineById(medicineId); + String studyInstanceUID = dataMedicine.getStudyInstanceUid(); + String seriesInstanceUID = dataMedicine.getSeriesInstanceUid(); + JSONObject mergeJSONObject = new JSONObject(); + mergeJSONObject.put("StudyInstanceUID", studyInstanceUID); + mergeJSONObject.put("seriesInstanceUID", seriesInstanceUID); + JSONArray jsonArrayMerge = new JSONArray(); + dataMedicineFiles.forEach(dataMedicineFile -> { + String targrtPath = StringUtils.substringBeforeLast(StringUtils.substringAfter(dataMedicineFile.getUrl(), + "/"), ".").replace("origin", "annotation") + ".json"; + InputStream inputStream = null; + try { + inputStream = minioUtil.getObjectInputStream(bucketName, targrtPath); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(inputStream); + Iterator elements = jsonNode.elements(); + JSONArray jsonArray = new JSONArray(); + while (elements.hasNext()) { + JsonNode next = elements.next(); + jsonArray.add(next.get("annotation").toString()); + } + jsonArrayMerge.add(jsonArray); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "get medicine annotation json failed, {}", e); + } finally { + try { + inputStream.close(); + } catch (IOException e) { + LogUtil.error(LogEnum.BIZ_DATASET, "close inputStream failed, {}", e); + } + } + }); + JSONObject jsonObjectTemp = new JSONObject(); + jsonObjectTemp.put("annotation", jsonArrayMerge); + String mergePointsString = jsonObjectTemp.getString("annotation").replace("\"", ""); + mergeJSONObject.put("annotation", mergePointsString); + String mergePath = StringUtils.substringBeforeLast(StringUtils.substringAfter(dataMedicineFiles.get(0).getUrl() + , "/"), "/").replace("origin", "annotation") + "/merge_annotation.json"; + try { + minioUtil.writeString(bucketName, mergePath, mergeJSONObject.toJSONString()); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "write merge_annotation failed, {}", e); + } + } + + /** + * 修改医学数据集和文件表中的修改人ID + * + * @param medicineId 医学数据集ID + */ + @Transactional(rollbackFor = Exception.class) + public void modifyUpdataUserId(Long medicineId){ + DataMedicine dataMedicine = dataMedicineService.getDataMedicineById(medicineId); + dataMedicine.setUpdateUserId(JwtUtils.getCurUserId()); + dataMedicineService.updateByMedicineId(dataMedicine); + dataMedicineFileService.updateUserIdByMedicineId(dataMedicine.getId()); + } + +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/task/DataMedicineRecycleFile.java b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/task/DataMedicineRecycleFile.java new file mode 100644 index 0000000..4372bfa --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/java/org/dubhe/dcm/service/task/DataMedicineRecycleFile.java @@ -0,0 +1,114 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ +package org.dubhe.dcm.service.task; + +import com.alibaba.fastjson.JSONObject; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.data.dao.DatasetLabelMapper; +import org.dubhe.data.dao.DatasetVersionFileMapper; +import org.dubhe.data.dao.FileMapper; +import org.dubhe.data.service.DatasetService; +import org.dubhe.dcm.service.DataMedicineFileService; +import org.dubhe.dcm.service.DataMedicineService; +import org.dubhe.recycle.domain.dto.RecycleCreateDTO; +import org.dubhe.recycle.domain.dto.RecycleDetailCreateDTO; +import org.dubhe.recycle.enums.RecycleTypeEnum; +import org.dubhe.recycle.global.AbstractGlobalRecycle; +import org.dubhe.recycle.utils.RecycleTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static org.dubhe.data.constant.Constant.LIMIT_NUMBER; + +/** + * @description 数据集文件删除类 + * @date 2021-03-03 + */ +@RefreshScope +@Component(value = "dataMedicineRecycleFile") +public class DataMedicineRecycleFile extends AbstractGlobalRecycle { + + @Value("${recycle.over-second.file}") + private long overSecond; + + + /** + * 数据集 service + */ + @Resource + private DataMedicineService dataMedicineService; + + /** + * 数据集 service + */ + @Resource + private DataMedicineFileService dataMedicineFileService; + + + @Autowired + private RecycleTool recycleTool; + + /** + * 根据数据集Id删除数据文件 + * + * @param detail 数据清理详情参数 + * @param dto 资源回收创建对象 + * @return true 继续执行,false 中断任务详情回收(本次无法执行完毕,创建新任务到下次执行) + */ + @Override + protected boolean clearDetail(RecycleDetailCreateDTO detail, RecycleCreateDTO dto) { + LogUtil.info(LogEnum.BIZ_DATASET, "DataMedicineRecycleFile.clear() , param:{}", JSONObject.toJSONString(detail)); + if (!Objects.isNull(detail.getRecycleCondition()) && RecycleTypeEnum.TABLE_DATA.getCode().compareTo(detail.getRecycleType()) == 0) { + //清理DB数据 + Long datasetId = Long.valueOf(detail.getRecycleCondition()); + dataMedicineService.deleteByDatasetId(datasetId); + dataMedicineFileService.deleteByDatasetId(datasetId); + }else { + //清理mino数据 + String recycleCondition = detail.getRecycleCondition(); + recycleTool.delTempInvalidResources(recycleCondition); + } + return true; + } + + + /** + * 数据还原 + * + * @param dto 数据清理参数 + */ + @Override + protected void rollback(RecycleCreateDTO dto) { + dataMedicineService.allRollback(dto); + } + + /** + * 覆盖数据集文件删除超时时间 + * @return 自定义超时秒 + */ + @Override + public long getRecycleOverSecond() { + return overSecond; + } +} diff --git a/dubhe-server/dubhe-data-dcm/src/main/resources/banner.txt b/dubhe-server/dubhe-data-dcm/src/main/resources/banner.txt new file mode 100644 index 0000000..6da4bbd --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _ _ _ _ + | | | | | | | | + __| |_ _| |__ | |__ ___ __| | ___ _ __ ___ + / _` | | | | '_ \| '_ \ / _ \ / _` |/ __| '_ ` _ \ +| (_| | |_| | |_) | | | | __/ | (_| | (__| | | | | | + \__,_|\__,_|_.__/|_| |_|\___| \__,_|\___|_| |_| |_| + + :: Dubhe org.dubhe.dcm :: 0.0.1-SNAPSHOT diff --git a/dubhe-server/dubhe-data-dcm/src/main/resources/bootstrap.yml b/dubhe-server/dubhe-data-dcm/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..46b2f81 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/resources/bootstrap.yml @@ -0,0 +1,50 @@ +server: + port: 8011 + # rest API 版本号 + rest-version: v1 + + +spring: + application: + name: dubhe-data-dcm + profiles: + active: dev + cloud: + nacos: + config: + enabled: true + server-addr: 127.0.0.1:8848 + namespace: dubhe-server-cloud-prod + shared-configs[0]: + data-id: common-biz.yaml + group: dubhe + refresh: true # 是否动态刷新,默认为false + shared-configs[1]: + data-id: common-recycle.yaml + group: dubhe + refresh: true + shared-configs[2]: + data-id: common-shardingjdbc.yaml + group: dubhe + refresh: true + shared-configs[3]: + data-id: dubhe-data.yaml + group: dubhe + refresh: true + shared-configs[4]: + data-id: common-k8s.yaml + group: dubhe + refresh: true + shared-configs[5]: + data-id: dubhe-data-dcm.yaml + group: dubhe + refresh: true + + discovery: + enabled: true + namespace: dubhe-server-cloud-prod + group: dubhe + server-addr: 127.0.0.1:8848 + # 配置允许后面的Bean覆盖前面名称重复的Bean + main: + allow-bean-definition-overriding: true \ No newline at end of file diff --git a/dubhe-server/dubhe-data-dcm/src/main/resources/mapper/DataMedicineFileMapper.xml b/dubhe-server/dubhe-data-dcm/src/main/resources/mapper/DataMedicineFileMapper.xml new file mode 100644 index 0000000..28d8c61 --- /dev/null +++ b/dubhe-server/dubhe-data-dcm/src/main/resources/mapper/DataMedicineFileMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + INSERT INTO data_medicine_file + ( + create_user_id, + update_user_id, + name, + status, + medicine_id, + url, + origin_user_id, + sop_instance_uid + ) + VALUES + + (#{item.createUserId}, #{item.updateUserId}, + #{item.name}, #{item.status}, #{item.medicineId}, + #{item.url}, #{item.originUserId}, #{item.sopInstanceUid}) + + + + \ No newline at end of file diff --git a/dubhe-server/dubhe-data-task/pom.xml b/dubhe-server/dubhe-data-task/pom.xml new file mode 100644 index 0000000..07ded47 --- /dev/null +++ b/dubhe-server/dubhe-data-task/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + + org.dubhe + server + 0.0.1-SNAPSHOT + + org.dubhe + dubhe-data-task + 0.0.1-SNAPSHOT + jar + Task 数据集定时任务 + 数据集定时任务模块 + + + + + org.dubhe.biz + base + ${org.dubhe.biz.base.version} + + + org.dubhe.biz + file + ${org.dubhe.biz.file.version} + + + org.dubhe.biz + data-permission + ${org.dubhe.biz.data-permission.version} + + + org.dubhe.biz + redis + ${org.dubhe.biz.redis.version} + + + + org.dubhe.cloud + registration + ${org.dubhe.cloud.registration.version} + + + + org.dubhe.cloud + configuration + ${org.dubhe.cloud.configuration.version} + + + + org.dubhe.cloud + remote-call + ${org.dubhe.cloud.remote-call.version} + + + + org.dubhe.biz + log + ${org.dubhe.biz.log.version} + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + + org.bytedeco + javacv + + + org.bytedeco.javacpp-presets + ffmpeg-platform + + + + org.apache.shardingsphere + sharding-jdbc-spring-boot-starter + ${sharding-jdbc} + + + org.dubhe + dubhe-data + ${org.dubhe.dubhe-data.version} + + + org.dubhe + dubhe-data-dcm + ${org.dubhe.dubhe-data-dcm.version} + + + com.alibaba + easyexcel + ${easyexcel} + + + net.sourceforge.javacsv + javacsv + ${javacsv} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + false + true + exec + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + diff --git a/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/DubheDataTaskApplication.java b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/DubheDataTaskApplication.java new file mode 100644 index 0000000..c659b9e --- /dev/null +++ b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/DubheDataTaskApplication.java @@ -0,0 +1,39 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.task; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @description 定时任务管理 + * @date 2020-12-18 + */ +@SpringBootApplication(scanBasePackages = "org.dubhe") +@MapperScan(basePackages = {"org.dubhe.**.dao"}) +@EnableScheduling +public class DubheDataTaskApplication { + + public static void main(String[] args) { + System.setProperty("es.set.netty.runtime.available.processors", "false"); + SpringApplication.run(DubheDataTaskApplication.class, args); + } + +} diff --git a/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationCopySchedule.java b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationCopySchedule.java new file mode 100644 index 0000000..cbc35a6 --- /dev/null +++ b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationCopySchedule.java @@ -0,0 +1,50 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.task.data; + +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.handler.ScheduleTaskHandler; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.data.service.DatasetVersionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * @description 标注文件复制 + * @date 2020-12-20 + */ +@Component +public class AnnotationCopySchedule { + + @Autowired + private DatasetVersionService datasetVersionService; + + /** + * 标注文件复制 + */ + @Scheduled(fixedDelay = 15000) + public void annotationFileCopy() { + ScheduleTaskHandler.process(() -> { + LogUtil.info(LogEnum.BIZ_DATASET, "annotation file copy and roll back --- > start"); + datasetVersionService.annotationFileCopy(); + LogUtil.info(LogEnum.BIZ_DATASET, "annotation file copy and roll back --- > end"); + }); + } + +} diff --git a/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationQueueExecuteThread.java b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationQueueExecuteThread.java new file mode 100644 index 0000000..bcc3a90 --- /dev/null +++ b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/AnnotationQueueExecuteThread.java @@ -0,0 +1,124 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.task.data; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.redis.utils.RedisUtils; +import org.dubhe.data.domain.bo.TaskSplitBO; +import org.dubhe.data.domain.dto.AnnotationInfoCreateDTO; +import org.dubhe.data.domain.dto.BatchAnnotationInfoCreateDTO; +import org.dubhe.data.service.AnnotationService; +import org.dubhe.data.util.TaskUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @description 自动标注完成队列处理 + * @date 2020-08-28 + */ +@Slf4j +@Component +public class AnnotationQueueExecuteThread implements Runnable { + + @Autowired + private RedisUtils redisUtils; + @Autowired + private AnnotationService annotationService; + @Autowired + private TaskUtils taskUtils; + + /** + * 标注算法执行中任务队列 + */ + private static final String ANNOTATION_START_QUEUE = "annotation_processing_queue"; + /** + * 标注算法未执行任务队列 + */ + private static final String ANNOTATION_PENDING_QUEUE = "annotation_task_queue"; + /** + * 标注算法已完成任务队列 + */ + private static final String ANNOTATION_FINISHED_QUEUE = "annotation_finished_queue"; + + /** + * 启动标注任务处理线程 + */ + @PostConstruct + public void start() { + Thread thread = new Thread(this, "自动标注完成任务处理队列"); + thread.start(); + } + + /** + * 标注任务处理方法 + */ + @Override + public void run() { + while (true) { + try { + Object object = redisUtils.lpop(ANNOTATION_FINISHED_QUEUE); + if (ObjectUtil.isNotNull(object)) { + Long start = System.currentTimeMillis(); + JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(redisUtils.get(object.toString()))); + String detailId = jsonObject.getString("reTaskId"); + JSONObject taskDetail = JSON.parseObject(JSON.toJSONString(redisUtils.get(detailId))); + // 得到一个任务的拆分 包括多个file + TaskSplitBO taskSplitBO = JSON.parseObject(JSON.toJSONString(taskDetail), TaskSplitBO.class); + JSONArray jsonArray = jsonObject.getJSONArray("annotations"); + + List list = new ArrayList<>(); + for (int i = 0; i < jsonArray.size(); i++) { + list.add(JSON.toJavaObject(jsonArray.getJSONObject(i), AnnotationInfoCreateDTO.class)); + } + BatchAnnotationInfoCreateDTO batchAnnotationInfoCreateDTO = new BatchAnnotationInfoCreateDTO(); + batchAnnotationInfoCreateDTO.setAnnotations(list); + annotationService.doFinishAuto(taskSplitBO, batchAnnotationInfoCreateDTO.toMap()); + redisUtils.del(detailId); + LogUtil.info(LogEnum.BIZ_DATASET, "the time it takes to perform a task is {} second", (System.currentTimeMillis() - start)); + TimeUnit.MILLISECONDS.sleep(MagicNumConstant.TEN); + } else { + TimeUnit.MILLISECONDS.sleep(MagicNumConstant.THREE_THOUSAND); + } + } catch (Exception exception) { + LogUtil.error(LogEnum.BIZ_DATASET, "annotation exception:{}", exception); + } + } + } + + /** + * annotation任务是否过期 + */ + @Scheduled(cron = "*/15 * * * * ?") + public void expireAnnotationTask() { + taskUtils.restartTask(ANNOTATION_START_QUEUE, ANNOTATION_PENDING_QUEUE); + } + +} diff --git a/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/DataTaskExecuteThread.java b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/DataTaskExecuteThread.java new file mode 100644 index 0000000..c017a3e --- /dev/null +++ b/dubhe-server/dubhe-data-task/src/main/java/org/dubhe/task/data/DataTaskExecuteThread.java @@ -0,0 +1,632 @@ +/** + * Copyright 2020 Tianshu AI Platform. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================= + */ + +package org.dubhe.task.data; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.bytedeco.javacv.FFmpegFrameGrabber; +import org.dubhe.biz.base.constant.DataStateCodeConstant; +import org.dubhe.biz.base.constant.MagicNumConstant; +import org.dubhe.biz.base.constant.NumberConstant; +import org.dubhe.biz.base.utils.StringUtils; +import org.dubhe.biz.log.enums.LogEnum; +import org.dubhe.biz.log.utils.LogUtil; +import org.dubhe.biz.redis.utils.RedisUtils; +import org.dubhe.biz.statemachine.dto.StateChangeDTO; +import org.dubhe.data.constant.Constant; +import org.dubhe.data.constant.DatasetLabelEnum; +import org.dubhe.data.domain.bo.TaskSplitBO; +import org.dubhe.data.domain.dto.DatasetEnhanceRequestDTO; +import org.dubhe.data.domain.dto.FileCreateDTO; +import org.dubhe.data.domain.dto.OfRecordTaskDto; +import org.dubhe.data.domain.entity.*; +import org.dubhe.biz.base.vo.DatasetVO; +import org.dubhe.data.machine.constant.DataStateMachineConstant; +import org.dubhe.data.machine.utils.StateMachineUtil; +import org.dubhe.data.pool.BasePool; +import org.dubhe.data.service.*; +import org.dubhe.data.util.TaskUtils; +import org.dubhe.dcm.domain.entity.DataMedicineFile; +import org.dubhe.dcm.service.DataMedicineFileService; +import org.dubhe.task.util.TableDataUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * @description 数据集任务处理方法(主要进行任务的拆解和分发) + * @date 2020-08-27 + */ +@Slf4j +@Component +public class DataTaskExecuteThread implements Runnable { + + @Autowired + private TaskService taskService; + @Autowired + private FileService fileService; + @Autowired + private DatasetService datasetService; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private RedisUtils redisUtils; + @Autowired + private DatasetLabelService datasetLabelService; + @Autowired + private DatasetVersionService datasetVersionService; + @Autowired + private DatasetVersionFileService datasetVersionFileService; + @Autowired + private AnnotationService annotationService; + @Autowired + private DatasetEnhanceService datasetEnhanceService; + @Autowired + private DataMedicineFileService dataMedicineFileService; + + @Resource + private TaskUtils taskUtils; + + @Value("${minio.bucketName}") + private String bucketName; + + @Value("${storage.file-store-root-path}") + private String nfsRootPath; + + @Autowired + private TableDataUtil tableDataUtil; + + /** + * 线程池 + */ + @Autowired + private BasePool pool; + + /** + * 路径名前缀 + */ + @Value("${storage.file-store-root-path:/nfs/}") + private String prefixPath; + /** + * 标注任务一次查询的数量 + */ + private static final Integer ANNOTATION_BATCH_SIZE = MagicNumConstant.SIXTEEN * MagicNumConstant.TEN_THOUSAND; + + /** + * 文本分类算法待处理任务队列 + */ + private static final String TC_TASK_QUEUE = "text_classification_task_queue"; + /** + * 标注算法待处理任务队列 + */ + private static final String ANNOTATION_TASK_QUEUE = "annotation_task_queue"; + /** + * ImageNet算法待处理任务队列 + */ + private static final String IMAGENET_TASK_QUEUE = "imagenet_task_queue"; + /** + * ofRecord算法待处理任务队列 + */ + private static final String OFRECORD_TASK_QUEUE = "ofrecord_task_queue"; + /** + * 目标跟踪算法待处理任务队列 + */ + private static final String TRACK_TASK_QUEUE = "track_task_queue"; + /** + * 医学标注算法待处理任务队列 + */ + private static final String MEDICINE_PENDING_QUEUE = "dcm_task_queue"; + + /** + * 启动生成任务线程 + */ + @PostConstruct + public void start() { + Thread thread = new Thread(this, "数据集任务生成"); + thread.start(); + } + + /** + * 生成任务run方法 + */ + @Override + public void run() { + while (true) { + try { + work(); + TimeUnit.MILLISECONDS.sleep(MagicNumConstant.ONE_THOUSAND); + } catch (Exception e) { + LogUtil.error(LogEnum.BIZ_DATASET, "get algorithm task failed:{}", e); + } + } + } + + /** + * 单个任务处理 + */ + public void work() { + // 获取一个待处理任务 + Task task = taskService.getOnePendingTask(); + if (ObjectUtil.isNotNull(task)) { + // 执行任务 + execute(task); + } + } + + /** + * 执行任务 + * + * @param task 任务详情 + */ + public void execute(Task task) { + // 任务加锁 + int count = taskService.updateTaskStatus(task.getId(), MagicNumConstant.ZERO, MagicNumConstant.ONE); + if (count != 0) { + switch (task.getType()) { + case MagicNumConstant.ZERO: + annotationExecute(NumberConstant.NUMBER_0,task); + break; + case MagicNumConstant.ONE: + ofRecordExecute(task); + break; + case MagicNumConstant.FOUR: + trackExecute(task); + break; + case MagicNumConstant.THREE: + enhanceExecute(task); + break; + case MagicNumConstant.FIVE: + videoSampleExecute(task); + break; + case MagicNumConstant.SIX: + medicineExecute(task); + break; + case MagicNumConstant.SEVEN: + textClassificationExecute(task); + break; + case MagicNumConstant.EIGHT: + annotationService.deleteAnnotating(task.getDatasetId()); + annotationExecute(NumberConstant.NUMBER_0,task); + break; + case MagicNumConstant.TEN: + csvImport(task); + break; + case MagicNumConstant.ELEVEN: + convertPreDataset(task); + default: + LogUtil.info(LogEnum.BIZ_DATASET, "未识别任务"); + break; + } + taskService.updateTaskStatus(task.getId(), MagicNumConstant.ONE, MagicNumConstant.TWO); + } + } + + /** + * 跟踪任务 + * + * @param task 任务详情 + */ + public void trackExecute(Task task) { + Dataset dataset = datasetService.getOneById(task.getDatasetId()); + Map> fileMap = annotationService.queryFileAccordingToCurrentVersionAndStatus(dataset); + List fileList = datasetVersionFileService.getFileListByVersionFileList(fileMap.get(task.getDatasetId())); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("path", nfsRootPath + bucketName + java.io.File.separator + dataset.getUri() + + (dataset.getCurrentVersionName() != null ? "/versionFile/" + dataset.getCurrentVersionName() : "")); + String taskId = UUID.randomUUID().toString(); + jsonObject.put("id", task.getId().toString()); + List images = new ArrayList<>(); + fileList.stream().forEach(file -> { + images.add(file.getUrl().substring(file.getUrl().lastIndexOf("/") + 1, file.getUrl().length())); + }); + jsonObject.put("images", images); + redisUtils.set(taskId, jsonObject); + redisUtils.zSet(TRACK_TASK_QUEUE, -1, taskId); + } + + + /** + * 标注任务处理 + * + * @param task 任务信息 + */ + public void textClassificationExecute(Task task) { + int offset = 0; + while (true) { + if (!generateTextClassificationTask(offset, task)) { + break; + } + } + } + + /** + * ofRecord转换任务处理 + * + * @param task 任务信息 + */ + public void ofRecordExecute(Task task) { + List