aws.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package regiondrivers
  15. import (
  16. "context"
  17. "fmt"
  18. "strings"
  19. "time"
  20. "unicode"
  21. "yunion.io/x/cloudmux/pkg/cloudprovider"
  22. "yunion.io/x/jsonutils"
  23. "yunion.io/x/pkg/errors"
  24. "yunion.io/x/pkg/util/secrules"
  25. "yunion.io/x/pkg/utils"
  26. api "yunion.io/x/onecloud/pkg/apis/compute"
  27. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  28. "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
  29. "yunion.io/x/onecloud/pkg/cloudcommon/validators"
  30. "yunion.io/x/onecloud/pkg/compute/models"
  31. "yunion.io/x/onecloud/pkg/httperrors"
  32. "yunion.io/x/onecloud/pkg/mcclient"
  33. "yunion.io/x/onecloud/pkg/util/logclient"
  34. "yunion.io/x/onecloud/pkg/util/seclib2"
  35. )
  36. type SAwsRegionDriver struct {
  37. SManagedVirtualizationRegionDriver
  38. }
  39. func init() {
  40. driver := SAwsRegionDriver{}
  41. models.RegisterRegionDriver(&driver)
  42. }
  43. func (self *SAwsRegionDriver) GetProvider() string {
  44. return api.CLOUD_PROVIDER_AWS
  45. }
  46. func (self *SAwsRegionDriver) IsSupportedDBInstance() bool {
  47. return true
  48. }
  49. func (self *SAwsRegionDriver) GetRdsSupportSecgroupCount() int {
  50. return 1
  51. }
  52. func (self *SAwsRegionDriver) IsSupportedElasticcache() bool {
  53. return true
  54. }
  55. func (self *SAwsRegionDriver) IsSupportedElasticcacheSecgroup() bool {
  56. return false
  57. }
  58. func (self *SAwsRegionDriver) GetMaxElasticcacheSecurityGroupCount() int {
  59. return 0
  60. }
  61. func (self *SAwsRegionDriver) ValidateCreateElasticcacheData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, input *api.ElasticcacheCreateInput) (*api.ElasticcacheCreateInput, error) {
  62. if len(input.Password) > 0 && len(input.Password) < 16 {
  63. passwd := seclib2.RandomPassword2(30)
  64. input.Password = ""
  65. for _, s := range passwd {
  66. if ok, _ := utils.InArray(s, []rune{'!', '&', '#', '$', '^', '<', '>', '-', '.'}); ok || unicode.IsDigit(s) || unicode.IsLetter(s) {
  67. input.Password += string(s)
  68. }
  69. }
  70. }
  71. return self.SManagedVirtualizationRegionDriver.ValidateCreateElasticcacheData(ctx, userCred, ownerId, input)
  72. }
  73. func (self *SAwsRegionDriver) ValidateCreateDBInstanceData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, input api.DBInstanceCreateInput, skus []models.SDBInstanceSku, network *models.SNetwork) (api.DBInstanceCreateInput, error) {
  74. if len(input.Password) > 0 {
  75. for _, s := range input.Password {
  76. if s == '/' || s == '"' || s == '@' || s == '\'' {
  77. return input, httperrors.NewInputParameterError("aws rds not support password character %s", string(s))
  78. }
  79. }
  80. }
  81. if len(input.Password) == 0 {
  82. for _, s := range seclib2.RandomPassword2(100) {
  83. if s == '/' || s == '"' || s == '@' || s == '\'' {
  84. continue
  85. }
  86. input.Password += string(s)
  87. if len(input.Password) >= 20 {
  88. break
  89. }
  90. }
  91. }
  92. return input, nil
  93. }
  94. func (self *SAwsRegionDriver) ValidateCreateDBInstanceBackupData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, instance *models.SDBInstance, input api.DBInstanceBackupCreateInput) (api.DBInstanceBackupCreateInput, error) {
  95. return input, nil
  96. }
  97. func (self *SAwsRegionDriver) ValidateCreateDBInstanceDatabaseData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, instance *models.SDBInstance, input api.DBInstanceDatabaseCreateInput) (api.DBInstanceDatabaseCreateInput, error) {
  98. return input, httperrors.NewNotSupportedError("aws not support create rds database")
  99. }
  100. func (self *SAwsRegionDriver) ValidateCreateDBInstanceAccountData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, instance *models.SDBInstance, input api.DBInstanceAccountCreateInput) (api.DBInstanceAccountCreateInput, error) {
  101. return input, httperrors.NewNotSupportedError("aws not support create rds account")
  102. }
  103. func (self *SAwsRegionDriver) InitDBInstanceUser(ctx context.Context, instance *models.SDBInstance, task taskman.ITask, desc *cloudprovider.SManagedDBInstanceCreateConfig) error {
  104. user := "admin"
  105. if desc.Engine == api.DBINSTANCE_TYPE_POSTGRESQL || desc.Category == api.DBINSTANCE_TYPE_POSTGRESQL {
  106. user = "postgres"
  107. }
  108. account := models.SDBInstanceAccount{}
  109. account.DBInstanceId = instance.Id
  110. account.Name = user
  111. account.Status = api.DBINSTANCE_USER_AVAILABLE
  112. account.SetModelManager(models.DBInstanceAccountManager, &account)
  113. err := models.DBInstanceAccountManager.TableSpec().Insert(ctx, &account)
  114. if err != nil {
  115. return err
  116. }
  117. return account.SetPassword(desc.Password)
  118. }
  119. func (self *SAwsRegionDriver) ValidateCreateLoadbalancerData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, input *api.LoadbalancerCreateInput) (*api.LoadbalancerCreateInput, error) {
  120. if len(input.LoadbalancerSpec) == 0 {
  121. input.LoadbalancerSpec = api.LB_AWS_SPEC_APPLICATION
  122. }
  123. if !utils.IsInStringArray(input.LoadbalancerSpec, api.LB_AWS_SPECS) {
  124. return nil, httperrors.NewInputParameterError("invalid loadbalancer_spec %s", input.LoadbalancerSpec)
  125. }
  126. return self.SManagedVirtualizationRegionDriver.ValidateCreateLoadbalancerData(ctx, userCred, ownerId, input)
  127. }
  128. func (self *SAwsRegionDriver) ValidateCreateLoadbalancerListenerData(ctx context.Context, userCred mcclient.TokenCredential,
  129. ownerId mcclient.IIdentityProvider, input *api.LoadbalancerListenerCreateInput,
  130. lb *models.SLoadbalancer, lbbg *models.SLoadbalancerBackendGroup) (*api.LoadbalancerListenerCreateInput, error) {
  131. input.Scheduler = api.LB_SCHEDULER_RR
  132. input.AclStatus = api.LB_BOOL_OFF
  133. input.StickySession = api.LB_BOOL_OFF
  134. return input, nil
  135. }
  136. func (self *SAwsRegionDriver) ValidateUpdateLoadbalancerListenerData(ctx context.Context, userCred mcclient.TokenCredential,
  137. lblis *models.SLoadbalancerListener, input *api.LoadbalancerListenerUpdateInput) (*api.LoadbalancerListenerUpdateInput, error) {
  138. return input, nil
  139. }
  140. func (self *SAwsRegionDriver) ValidateCreateLoadbalancerListenerRuleData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, input *api.LoadbalancerListenerRuleCreateInput) (*api.LoadbalancerListenerRuleCreateInput, error) {
  141. segs := []string{}
  142. if len(input.Path) > 0 {
  143. segs = append(segs, fmt.Sprintf(`{"field":"path-pattern","pathPatternConfig":{"values":["%s"]},"values":["%s"]}`, input.Path, input.Path))
  144. }
  145. if len(input.Domain) > 0 {
  146. segs = append(segs, fmt.Sprintf(`{"field":"host-header","hostHeaderConfig":{"values":["%s"]},"values":["%s"]}`, input.Domain, input.Domain))
  147. }
  148. input.Condition = fmt.Sprintf(`[%s]`, strings.Join(segs, ","))
  149. err := models.ValidateListenerRuleConditions(input.Condition)
  150. if err != nil {
  151. return nil, httperrors.NewInputParameterError("%s", err)
  152. }
  153. return input, nil
  154. }
  155. func (self *SAwsRegionDriver) ValidateUpdateLoadbalancerListenerRuleData(ctx context.Context, userCred mcclient.TokenCredential, input *api.LoadbalancerListenerRuleUpdateInput) (*api.LoadbalancerListenerRuleUpdateInput, error) {
  156. return input, nil
  157. }
  158. func (self *SAwsRegionDriver) ValidateCreateLoadbalancerBackendData(ctx context.Context, userCred mcclient.TokenCredential,
  159. lb *models.SLoadbalancer, lbbg *models.SLoadbalancerBackendGroup,
  160. input *api.LoadbalancerBackendCreateInput) (*api.LoadbalancerBackendCreateInput, error) {
  161. return self.SManagedVirtualizationRegionDriver.ValidateCreateLoadbalancerBackendData(ctx, userCred, lb, lbbg, input)
  162. }
  163. func (self *SAwsRegionDriver) ValidateUpdateLoadbalancerBackendData(ctx context.Context, userCred mcclient.TokenCredential, lbbg *models.SLoadbalancerBackendGroup, input *api.LoadbalancerBackendUpdateInput) (*api.LoadbalancerBackendUpdateInput, error) {
  164. // 不能更新端口和权重
  165. if input.Port != nil {
  166. return input, fmt.Errorf("can not update backend port.")
  167. }
  168. if input.Weight != nil {
  169. return input, fmt.Errorf("can not update backend weight.")
  170. }
  171. return input, nil
  172. }
  173. func (self *SAwsRegionDriver) RequestCreateLoadbalancerListener(ctx context.Context, userCred mcclient.TokenCredential, lblis *models.SLoadbalancerListener, task taskman.ITask) error {
  174. taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) {
  175. provider := lblis.GetCloudprovider()
  176. if provider == nil {
  177. return nil, fmt.Errorf("failed to find provider for lblis %s", lblis.Name)
  178. }
  179. lbbg, err := lblis.GetLoadbalancerBackendGroup()
  180. if err != nil {
  181. return nil, errors.Wrapf(err, "GetLoadbalancerBackendGroup")
  182. }
  183. lb, err := lblis.GetLoadbalancer()
  184. if err != nil {
  185. return nil, errors.Wrapf(err, "GetLoadbalancer")
  186. }
  187. iLb, err := lb.GetILoadbalancer(ctx)
  188. if err != nil {
  189. return nil, errors.Wrapf(err, "GetILoadbalancer")
  190. }
  191. opts, err := lblis.GetLoadbalancerListenerParams()
  192. if err != nil {
  193. return nil, errors.Wrapf(err, "GetLoadbalancerListenerParams")
  194. }
  195. {
  196. if lblis.ListenerType == api.LB_LISTENER_TYPE_HTTPS && len(lblis.CertificateId) > 0 {
  197. cert, err := lblis.GetCertificate()
  198. if err != nil {
  199. return nil, errors.Wrapf(err, "GetCertificate")
  200. }
  201. opts.CertificateId = cert.ExternalId
  202. }
  203. }
  204. if len(lbbg.ExternalId) == 0 {
  205. vpc, err := lb.GetVpc()
  206. if err != nil {
  207. return nil, errors.Wrapf(err, "GetVpc")
  208. }
  209. lbbgOpts := &cloudprovider.SLoadbalancerBackendGroup{
  210. Name: lbbg.Name,
  211. Scheduler: lblis.Scheduler,
  212. Protocol: lblis.ListenerType,
  213. ListenPort: lblis.ListenerPort,
  214. VpcId: vpc.ExternalId,
  215. }
  216. iLbbg, err := iLb.CreateILoadBalancerBackendGroup(lbbgOpts)
  217. if err != nil {
  218. return nil, errors.Wrapf(err, "CreateILoadBalancerBackendGroup")
  219. }
  220. err = db.SetExternalId(lbbg, userCred, iLbbg.GetGlobalId())
  221. if err != nil {
  222. return nil, errors.Wrapf(err, "db.SetExternalId")
  223. }
  224. opts.BackendGroupId = iLbbg.GetGlobalId()
  225. }
  226. iLis, err := iLb.CreateILoadBalancerListener(ctx, opts)
  227. if err != nil {
  228. return nil, errors.Wrapf(err, "CreateILoadBalancerListener")
  229. }
  230. err = db.SetExternalId(lblis, userCred, iLis.GetGlobalId())
  231. if err != nil {
  232. return nil, errors.Wrapf(err, "lblis.SetExternalId")
  233. }
  234. backends, err := lbbg.GetBackends()
  235. if err != nil {
  236. return nil, errors.Wrapf(err, "GetBackends")
  237. }
  238. if len(backends) == 0 {
  239. return nil, nil
  240. }
  241. iLbbg, err := lbbg.GetICloudLoadbalancerBackendGroup(ctx)
  242. if err != nil {
  243. return nil, errors.Wrapf(err, "GetICloudLoadbalancerBackendGroup")
  244. }
  245. for i := range backends {
  246. opts := &cloudprovider.SLoadbalancerBackend{
  247. Weight: backends[i].Weight,
  248. Port: backends[i].Port,
  249. ExternalId: backends[i].ExternalId,
  250. }
  251. _, err := iLbbg.AddBackendServer(opts)
  252. if err != nil {
  253. return nil, errors.Wrapf(err, "AddBackendServer")
  254. }
  255. }
  256. return nil, nil
  257. })
  258. return nil
  259. }
  260. func (self *SAwsRegionDriver) RequestStartLoadbalancerListener(ctx context.Context, userCred mcclient.TokenCredential, lblis *models.SLoadbalancerListener, task taskman.ITask) error {
  261. return task.ScheduleRun(nil)
  262. }
  263. func (self *SAwsRegionDriver) RequestCreateLoadbalancerBackendGroup(ctx context.Context, userCred mcclient.TokenCredential, lbbg *models.SLoadbalancerBackendGroup, task taskman.ITask) error {
  264. return task.ScheduleRun(nil)
  265. }
  266. func (self *SAwsRegionDriver) IsCertificateBelongToRegion() bool {
  267. return false
  268. }
  269. func (self *SAwsRegionDriver) ValidateCreateVpcData(ctx context.Context, userCred mcclient.TokenCredential, input api.VpcCreateInput) (api.VpcCreateInput, error) {
  270. cidrV := validators.NewIPv4PrefixValidator("cidr_block")
  271. if err := cidrV.Validate(ctx, jsonutils.Marshal(input).(*jsonutils.JSONDict)); err != nil {
  272. return input, err
  273. }
  274. if cidrV.Value.MaskLen < 16 || cidrV.Value.MaskLen > 28 {
  275. return input, httperrors.NewInputParameterError("%s request the mask range should be between 16 and 28", self.GetProvider())
  276. }
  277. return input, nil
  278. }
  279. func (self *SAwsRegionDriver) RequestAssociateEip(ctx context.Context, userCred mcclient.TokenCredential, eip *models.SElasticip, input api.ElasticipAssociateInput, obj db.IStatusStandaloneModel, task taskman.ITask) error {
  280. taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) {
  281. iEip, err := eip.GetIEip(ctx)
  282. if err != nil {
  283. return nil, errors.Wrapf(err, "eip.GetIEip")
  284. }
  285. conf := &cloudprovider.AssociateConfig{
  286. InstanceId: input.InstanceExternalId,
  287. Bandwidth: eip.Bandwidth,
  288. AssociateType: api.EIP_ASSOCIATE_TYPE_SERVER,
  289. }
  290. err = iEip.Associate(conf)
  291. if err != nil {
  292. return nil, errors.Wrapf(err, "iEip.Associate")
  293. }
  294. err = cloudprovider.WaitStatus(iEip, api.EIP_STATUS_READY, 3*time.Second, 60*time.Second)
  295. if err != nil {
  296. return nil, errors.Wrap(err, "cloudprovider.WaitStatus")
  297. }
  298. if obj.GetStatus() != api.INSTANCE_ASSOCIATE_EIP {
  299. db.StatusBaseSetStatus(ctx, obj, userCred, api.INSTANCE_ASSOCIATE_EIP, "associate eip")
  300. }
  301. err = eip.AssociateInstance(ctx, userCred, input.InstanceType, obj)
  302. if err != nil {
  303. return nil, errors.Wrapf(err, "eip.AssociateVM")
  304. }
  305. if input.InstanceType == api.EIP_ASSOCIATE_TYPE_SERVER {
  306. // 如果aws已经绑定了EIP,则要把多余的公有IP删除
  307. if iEip.GetMode() == api.EIP_MODE_STANDALONE_EIP {
  308. server := obj.(*models.SGuest)
  309. publicIP, err := server.GetPublicIp()
  310. if err != nil {
  311. return nil, errors.Wrap(err, "AwsGuestDriver.GetPublicIp")
  312. }
  313. if publicIP != nil {
  314. err = db.DeleteModel(ctx, userCred, publicIP)
  315. if err != nil {
  316. return nil, errors.Wrap(err, "AwsGuestDriver.DeletePublicIp")
  317. }
  318. }
  319. }
  320. }
  321. eip.SetStatus(ctx, userCred, api.EIP_STATUS_READY, "associate")
  322. return nil, nil
  323. })
  324. return nil
  325. }
  326. func (self *SAwsRegionDriver) ValidateCreateWafInstanceData(ctx context.Context, userCred mcclient.TokenCredential, input api.WafInstanceCreateInput) (api.WafInstanceCreateInput, error) {
  327. if len(input.Type) == 0 {
  328. input.Type = cloudprovider.WafTypeRegional
  329. }
  330. switch input.Type {
  331. case cloudprovider.WafTypeRegional:
  332. case cloudprovider.WafTypeCloudFront:
  333. _region, err := models.CloudregionManager.FetchById(input.CloudregionId)
  334. if err != nil {
  335. return input, err
  336. }
  337. region := _region.(*models.SCloudregion)
  338. if !strings.HasSuffix(region.ExternalId, "us-east-1") {
  339. return input, httperrors.NewUnsupportOperationError("only us-east-1 support %s", input.Type)
  340. }
  341. default:
  342. return input, httperrors.NewInputParameterError("Invalid aws waf type %s", input.Type)
  343. }
  344. if input.DefaultAction == nil {
  345. input.DefaultAction = &cloudprovider.DefaultAction{
  346. Action: cloudprovider.WafActionAllow,
  347. }
  348. }
  349. return input, nil
  350. }
  351. func (self *SAwsRegionDriver) ValidateCreateSecurityGroupInput(ctx context.Context, userCred mcclient.TokenCredential, input *api.SSecgroupCreateInput) (*api.SSecgroupCreateInput, error) {
  352. for i := range input.Rules {
  353. if input.Rules[i].Action != string(secrules.SecurityRuleAllow) {
  354. return nil, httperrors.NewInputParameterError("invalid action %s, only support allow", input.Rules[i].Action)
  355. }
  356. if len(input.Rules[i].Ports) > 0 && strings.Contains(input.Rules[i].Ports, ",") {
  357. return nil, httperrors.NewInputParameterError("invalid ports %s", input.Rules[i].Ports)
  358. }
  359. }
  360. return self.SManagedVirtualizationRegionDriver.ValidateCreateSecurityGroupInput(ctx, userCred, input)
  361. }
  362. func (self *SAwsRegionDriver) ValidateUpdateSecurityGroupRuleInput(ctx context.Context, userCred mcclient.TokenCredential, input *api.SSecgroupRuleUpdateInput) (*api.SSecgroupRuleUpdateInput, error) {
  363. if input.Action != nil && *input.Action != string(secrules.SecurityRuleAllow) {
  364. return nil, httperrors.NewInputParameterError("invalid action %s", *input.Action)
  365. }
  366. if input.Ports != nil && strings.Contains(*input.Ports, ",") {
  367. return nil, httperrors.NewInputParameterError("invalid ports %s", *input.Ports)
  368. }
  369. return self.SManagedVirtualizationRegionDriver.ValidateUpdateSecurityGroupRuleInput(ctx, userCred, input)
  370. }
  371. func (self *SAwsRegionDriver) RequestRemoteUpdateElasticcache(ctx context.Context, userCred mcclient.TokenCredential, elasticcache *models.SElasticcache, replaceTags bool, task taskman.ITask) error {
  372. taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) {
  373. iRegion, err := elasticcache.GetIRegion(ctx)
  374. if err != nil {
  375. return nil, errors.Wrap(err, "elasticcache.GetIRegion")
  376. }
  377. iElasticcache, err := iRegion.GetIElasticcacheById(elasticcache.ExternalId)
  378. if err != nil {
  379. return nil, errors.Wrapf(err, "GetIElasticcacheById(%s)", elasticcache.ExternalId)
  380. }
  381. oldTags, err := iElasticcache.GetTags()
  382. if err != nil {
  383. if errors.Cause(err) == cloudprovider.ErrNotSupported || errors.Cause(err) == cloudprovider.ErrNotImplemented {
  384. return nil, nil
  385. }
  386. return nil, errors.Wrap(err, "iElasticcache.GetTags()")
  387. }
  388. tags, err := elasticcache.GetAllUserMetadata()
  389. if err != nil {
  390. return nil, errors.Wrapf(err, "GetAllUserMetadata")
  391. }
  392. tagsUpdateInfo := cloudprovider.TagsUpdateInfo{OldTags: oldTags, NewTags: tags}
  393. mangerId := ""
  394. if vpc, _ := elasticcache.GetVpc(); vpc != nil {
  395. mangerId = vpc.ManagerId
  396. }
  397. err = cloudprovider.SetTags(ctx, iElasticcache, mangerId, tags, replaceTags)
  398. if err != nil {
  399. if errors.Cause(err) == cloudprovider.ErrNotSupported || errors.Cause(err) == cloudprovider.ErrNotImplemented {
  400. return nil, nil
  401. }
  402. logclient.AddActionLogWithStartable(task, elasticcache, logclient.ACT_UPDATE_TAGS, err, userCred, false)
  403. return nil, errors.Wrap(err, "iElasticcache.SetTags")
  404. }
  405. cloudprovider.WaitMultiStatus(iElasticcache, []string{api.ELASTIC_CACHE_STATUS_RUNNING}, 15*time.Second, 2*time.Minute)
  406. logclient.AddActionLogWithStartable(task, elasticcache, logclient.ACT_UPDATE_TAGS, tagsUpdateInfo, userCred, true)
  407. return nil, nil
  408. })
  409. return nil
  410. }